fix(babysitter): propose from a private git dir, never the checkout's .git

Merged
KH
5d ago
+960-129

Live run #1 of the Babysitter fixer (Cloud run c5c7bb01-2d31-59f4-b907-ea1bad59046b, AgentWorkforce/cloud-e2e-sandbox#49) got as far as a correct fix:

  • agent-6, claude-sonnet-5, rejected a bad attempts with RangeError and added a test.
  • The propose step then failed in 36 ms:
exit code 1: EACCES: permission denied, open '/home/user/project/workflows/runs/da2fc0ff-…/babysitter-checkout/.git/config'

(from GET /api/v1/workflows/runs/<id>/steps)

proposeChanges hardened against an agent-planted git config by rewriting the checkout's .git/config (and deleting hooks and info/attributes). The Cloud sandbox leaves .git unwritable after the agent step, so that write failed and nothing was published. The run failed closed, but uselessly. The fixer merged in #644 (469baf18) has the same code.

Change (examples/babysitter/fix.ts, proposeChanges only)

git never uses the checkout's .git as a repository any more. Each proposal:

  • runs in a fresh private git dir (mkdtemp under the OS temp dir, removed in finally);
  • borrows the checkout's objects read-only through objects/info/alternates;
  • copies the checkout's index, so unmerged stages for the conflict guards and the agent's staged state behave exactly as before;
  • uses the checkout as --work-tree and cwd, so path arguments resolve the same way as under the old git -C <checkout>.

An agent-planted config, hook or info/attributes in the checkout is never read, which is stronger than rewriting them, and nothing in the checkout's .git is written. The existing environment hardening is unchanged: GIT_CONFIG_NOSYSTEM, GIT_CONFIG_GLOBAL=/dev/null, core.hooksPath=/dev/null, core.quotePath=false.

New fixer digest: f107bcf907bb7f695fcdd2e21f23030a81a0ce5292dc0455374fddd5ac13f2cc. The diagnose artifact is unchanged at 580bab1d…. Cloud's pin in AgentWorkforce/cloud#4434 must move to it.

Tests

New test: "proposes from a checkout whose .git the sandbox left unwritable, touching nothing in it". It chmods .git read-only after an agent-style edit, then asserts:

  • a proposal containing the edit;
  • .git/config unchanged;
  • no private git dir left behind.

Mutation-verified with fix.ts from origin/main restored (it reproduces the production error exactly):

$ (cd examples/babysitter && node --experimental-strip-types --test tests/task.test.ts)   # fix.ts at origin/main (rewrites .git/config)
    code: 'EACCES',
  Error: EACCES: permission denied, open '/home/khaliqgant/.agent-tmp/babysitter-checkout-wza2lF/.git/config'
ℹ fail 1
ℹ pass 24
✖ proposes from a checkout whose .git the sandbox left unwritable, touching nothing in it (166.662091ms)
restored sha ok: yes
$ (cd examples/babysitter && node --experimental-strip-types --test tests/task.test.ts)   # private git dir
ℹ pass 25
ℹ fail 0
  • Babysitter suite: 170 tests, 163 pass, 0 fail, 7 todo. The conflict guards, the merge rules and "never run git config the agent planted" all still pass.
  • build-standalone.mjs --check OK.
  • flows check on the fixer: CHECK PASSED; it still requires github, codex and claude.
  • Shipped-source model audit: 2/2.

While developing, an intermediate version without cwd silently disarmed the conflict guards (relative pathspecs resolved against the run root). The existing conflict tests caught it, and cwd is the fix.

🤖 Generated with Claude Code


[!NOTE]
Medium Risk
Changes trusted git invocation for agent proposal generation (security-sensitive path); behavior is heavily tested but incorrect index/alternates handling could mis-publish or refuse merges.

Overview
Fixes the Babysitter fixer propose step failing with EACCES when the Cloud sandbox leaves the checkout's .git unwritable after the agent run. The old approach rewrote .git/config and deleted hooks/attributes to ignore agent tampering; that write is what broke production.

proposeChanges now runs all git commands against a temporary private git directory (cleaned up in finally) that shares the checkout's object store via alternates, copies index and sharedindex.* (regular files only, preserving mtime for racy-git correctness), and uses the checkout as --work-tree with cwd set so pathspecs and conflict guards behave like the prior git -C flow. The checkout's .git is neither read for config nor written.

Adds task tests (read-only .git, split index, decoy index names, scratch cleanup, symlinked/missing index) plus mutation and racy-index evidence scripts. The shipped fixer artifact digest updates to f107bcf9….

Reviewed by Cursor Bugbot for commit 742c7272bda44af3763a833e8e178aab1e97c48e. Bugbot is set up for automated code reviews on this repo. Configure here.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes the babysitter propose step failing with EACCES when the Cloud sandbox leaves the checkout's .git unwritable after the agent step. Previously proposeChanges hardened against agent-planted git config by rewriting .git/config and deleting hooks and info/attributes; that write now fails and nothing gets published.

Git now runs in a fresh private git dir that borrows the checkout's objects read-only via objects/info/alternates and copies its index, with the checkout as work-tree and cwd. Nothing in the checkout's .git is written or read as configuration anymore. The copy keeps the index's mtime so racy git can't miss a same-size edit made in the same timestamp tick as the index, carries over the sharedindex.* files a split index needs, and only copies regular files so a FIFO, directory, or symlink with one of those names can't block or throw. A missing or symlinked active index refuses instead of hiding an unresolved conflict. Setup failures (or a later failure) leave no scratch dir behind.

New tests cover a read-only .git, a split index, decoy FIFO/dir/symlink sharedindex.* names, a missing or symlinked index, and scratch cleanup when setup fails (the setup-failure case uses a .git file, so it fails even when run as root). The mutation and racy-index evidence come from committed scripts that print each command shell-escaped, so they reproduce verbatim from a checkout. The fixer digest is now f107bcf9…; Cloud's pin needs to move to it.


Agent Relay sessions

  • claude session 497569fc · opened via gh pr create · last active 2026-10-10
  • claude session 5863fddb · contributor · ran gh pr commands · last active 2026-10-10