ci: prevent the coding agent from working in a discarded worktree - #4835
ci: prevent the coding agent from working in a discarded worktree#4835Behzad Mirkhanzadeh (behzad-mir) wants to merge 1 commit into
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
🟡 Changes recommended
The newly added “MANDATORY” shell snippets should explicitly guard against an unset $GITHUB_WORKSPACE and ensure the base ref exists so the checks fail deterministically for the intended reasons.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR updates contributor/agent guidance to prevent GitHub Copilot’s cloud coding agent (running in GitHub Actions) from doing work in a discarded git worktree, which previously resulted in empty upgrade PRs despite substantial work being performed.
Changes:
- Add an explicit exception in
agents.mdclarifying that the cloud coding agent must work directly in$GITHUB_WORKSPACE(no worktrees). - Add “working directory” and “non-empty branch” guardrails to the
acn-go-version-bumpskill to prevent claiming completion when no changes are actually committed.
File summaries
| File | Description |
|---|---|
agents.md |
Documents the cloud-agent exception to the local worktree-only workflow so work isn’t lost outside $GITHUB_WORKSPACE. |
.github/skills/acn-go-version-bump/SKILL.md |
Adds mandatory checks to ensure work happens in the correct directory and the branch contains real diffs before reporting done. |
Review details
Suppressed comments (1)
.github/skills/acn-go-version-bump/SKILL.md:637
- In the final “branch is non-empty” verification block,
cd "$GITHUB_WORKSPACE"has the same failure mode as above (empty var →cdto$HOME), andorigin/mastermay not exist in a shallow checkout. Add a guard and ensure the base ref is present before running the diffs, so the check fails for the right reason.
cd "$GITHUB_WORKSPACE"
git status --short
git diff --stat origin/master...HEAD
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| This has already caused two empty PRs (#4756, #4833). Verify before you start: | ||
|
|
||
| ```bash | ||
| cd "$GITHUB_WORKSPACE" && pwd && git rev-parse --show-toplevel |
f3feeae to
131cf90
Compare
The Copilot coding agent opened #4756 and #4833 with detailed descriptions and zero changed files. Both reported success and CI was green, so the failure was silent. Log forensics on the working directory of every tool call: run 33692935610 (#4833) worktree -> 0 files run 32806937007 (#4756) worktree -> 0 files run 32814359584 (fork #49) workspace -> 46 files The agent did real work -- edits, make dockerfiles, go build, go vet, git add . -- inside a git worktree under session-state. Its commit/push step only sees $GITHUB_WORKSPACE, so all of it was discarded. agents.md section 6 mandates worktrees and declares the repo root read-only. That is correct for local CLI sessions, where one clone is shared across concurrent sessions, and wrong for the cloud agent, which gets an ephemeral single-session container. An earlier theory blamed the firewall. It was wrong: the successful fork run had 8 blocked domains, the failing run had 1 (telemetry only). Instructions alone are not a control here. The same agents.md was present for all three runs and produced worktree behavior twice and workspace behavior once, so the fork validation proved nothing. copilot-setup-steps.yml therefore installs a git shim at /usr/bin/git and /usr/local/bin/git, with the real binary preserved at /usr/local/lib/git-guard/git.real. Installing at both entry points means every PATH-resolved git for the rest of the job, including the agent phase. It rejects 'worktree add' with a message redirecting to $GITHUB_WORKSPACE and execs real git for everything else. Shadowing by PATH position means it does not depend on $GITHUB_PATH or $GITHUB_ENV propagating into that phase. Argument parsing skips global options that consume a value (-C, -c, --git-dir, ...). Without that, 'git -C <path> worktree add' treats the path as the subcommand and bypasses the guard -- a real hole found while testing the first version. The step self-tests and fails setup if the shim does not resolve first, does not block all five probes, or leaks a directory, so it cannot end up silently unguarded. Verified on ubuntu-latest in run 33695179506: blocked worktree add / add -b / -C <path> worktree add / --no-pager worktree add / -c a=b -C <path> worktree add / --git-dir <path> worktree add passes --version, status, worktree list, -C <path> worktree list, log, -c core.pager=cat diff, commit -m 'worktree add stuff' (not a false positive) The documentation changes scope the worktree rule to local CLI sessions. copilot-instructions.md carries the carve-out above the line that pulls agents.md in, since that line is how the rule reached the cloud agent. SKILL.md adds a working-directory check and forbids reporting completion on an empty diff. None of these are load-bearing; correctness rests on the shim. Verification after merge: re-run #4832 and confirm the agent log has zero tool.execution_start lines under a worktrees path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b043d9fe-4797-42bf-9482-c337444c5a6f Hardening: the shim is installed at /usr/bin/git as well as /usr/local/bin/git. A PATH-position-only shim was bypassable with an explicit '/usr/bin/git worktree add', demonstrated locally. The real binary is preserved at /usr/local/lib/git-guard/git.real, copied once under a guard so a re-run cannot overwrite it with the shim. /usr/lib/git-core/git is deliberately left untouched so git's internal helper dispatch is unaffected. Self-test now covers 8 spellings including both absolute paths.
131cf90 to
6ee4a2a
Compare
|
|
||
| ### Working Directory (MANDATORY — read before you edit anything) | ||
|
|
||
| **If you are the GitHub Copilot coding agent running on a GitHub Actions runner, do ALL work in `$GITHUB_WORKSPACE`. Do NOT create a git worktree.** |
There was a problem hiding this comment.
could we just say - "If $GITHUB_WORKSPACE is set, use it instead of worktree"
|
|
||
| `agents.md` instructs local CLI sessions to work in a dedicated worktree because the local repo root is shared between concurrent sessions. That does **not** apply to you: the cloud runner is a single-session ephemeral container, and your commit/push step only sees `$GITHUB_WORKSPACE`. Work done in a worktree elsewhere on disk is **silently discarded**, producing a PR with a detailed description and zero files. | ||
|
|
||
| This has already caused two empty PRs (#4756, #4833). Verify before you start: |
| This has already caused two empty PRs (#4756, #4833). Verify before you start: | ||
|
|
||
| ```bash | ||
| cd "$GITHUB_WORKSPACE" && pwd && git rev-parse --show-toplevel |
| # The coding agent has twice done all its work inside a git worktree | ||
| # outside $GITHUB_WORKSPACE (#4756, #4833). Only $GITHUB_WORKSPACE is |
There was a problem hiding this comment.
Cut, these should be stateless, portable instructions, not an RCA referencing PRs
Problem
The Copilot coding agent has opened two pull requests with a detailed description and zero changed files — #4756 and #4833. Both reported success. All checks were green.
Log forensics on the working directory of every tool call:
.../session-state/<id>/files/worktrees/go-127-upgrade$GITHUB_WORKSPACEThe agent did real work — edits,
make dockerfiles,go build,go vet,git add .— inside a git worktree. The commit/push step only sees$GITHUB_WORKSPACE, so all of it was discarded.agents.md§6 mandates worktree use and declares the repo root read-only. That is correct for local Copilot CLI sessions, where one clone is shared between concurrent sessions. It is wrong for the cloud agent, which gets an ephemeral single-session container.An earlier theory blamed the firewall. That was wrong: the successful fork run had 8 blocked domains, the failing run had 1 (telemetry).
Why instructions are not enough
The same
agents.mdwas present for all three runs. It produced worktree behavior twice and non-worktree behavior once. The fork run succeeded by luck, not configuration — which is why validating the earlier fix on the fork proved nothing.Fix
copilot-setup-steps.ymlinstalls agitshim at/usr/local/bin/git. That path precedes/usr/binin the runner PATH, so it intercepts every PATH-resolvedgitfor the rest of the job, including the agent phase. It does not rely on$GITHUB_PATH/$GITHUB_ENVpropagating.git worktree add→ exits 1 with a message pointing at$GITHUB_WORKSPACEexec /usr/bin/git "$@"Argument parsing skips global options that consume a value (
-C,-c,--git-dir, ...). Without this,git -C <path> worktree addtreats the path as the subcommand and bypasses the guard — a real hole found while testing the first version.The step self-tests and fails setup if the shim does not resolve first, does not block all five
worktree addspellings, or leaks a directory. It cannot silently end up unguarded.Verified:
Also
.github/copilot-instructions.md— carve-out placed above the line that pulls inagents.md, so the agent knows where to work before it hits the block. Correctness no longer depends on it.agents.md§6 /SKILL.md— scoped the worktree rule to local CLI sessions.Not included
An earlier commit in this branch added an empty-PR detector. Removed — it caught the failure after the fact instead of preventing it.
Verification after merge
Re-run #4832. In the agent log,
grep -c worktreesovertool.execution_startlines should be0.