Skip to content

Latest commit

 

History

History
301 lines (238 loc) · 14.7 KB

File metadata and controls

301 lines (238 loc) · 14.7 KB

AGENTS.md

Instructions for any coding agent (Claude, Codex, Cursor, hermes-agent, OpenClaw, future-model) checking out cheshirecode/sandbox. Written from real bugs that bit during this repo's own construction; not aspirational norms.

What this repo is

cheshirecode/sandbox is an ephemeral Docker dev container that gives a fresh shell with the user's personal GitHub identity (cheshirecode) auto-piped from the host — never the user's work identity. Designed to host work on other cheshirecode/* repos without leaking work credentials, without polluting the host OS with apt installs, and without manual login to Claude Code / Codex inside the container.

One repo, one container, one shell. Not a multi-tenant platform. Not a production runtime.

The non-negotiable invariants

Every change must preserve these. Tests enforce them; the entrypoint refuses to start if they're violated.

  1. No work identity in the container, ever. Entrypoint refuses to start if $GITHUB_TOKEN, $GH_ENTERPRISE_TOKEN, or any var matching $SANDBOX_REFUSE_PATTERNS is set. Test #1/#2 in tests/run.sh.

  2. Credentials transit via tmpfs /run/secrets/*, never via -e, --build-arg, or docker inspect-visible state. Entrypoint reads then shred -us. Test #4b/#4e.

  3. HTTPS-only git remotes inside the container. SSH-agent forwarding is never enabled — it would tunnel the user's work SSH key. Test #5.

  4. gpgsign=false inside container. Signing on the host, not inside the sandbox. Test #7.

  5. gh auth setup-git runs in entrypoint so git push https://github.com/... doesn't prompt for username. Dogfood-discovered (see "Hazards" § credential-handling).

  6. Container runs --user dev, not root. Production runtime flag — every test that exercises the container MUST also pass --user dev, otherwise it misses a whole class of permission bugs (see Hazards § --user dev blind-spot).

Quick-start for a coding agent

If you're a fresh agent assigned to do work involving cheshirecode/* repos, the canonical flow is:

# 0. Cd into this repo
cd ~/Documents/oss/sandbox    # or wherever the user clones it

# 1. Load personal-OSS gh identity BEFORE up (hazard #13 — host leak)
source ~/Documents/oss/.envrc
export WORKLOG_LDAP=cheshirecode

# 2. Bring the container up (auto-pipes Anthropic + Codex creds from host)
bin/sandbox.sh up --no-attach

# 3. OSS workspace repos (bind-mounted at /workspace/oss/<name>)
#    Edit on host; test inside container without cloning.

# 3b. Ideogram-internal repos under ~/Documents/projects → /workspace/projects
#     Edit + commit on host (projects/.envrc); sandbox is verify-only:
docker exec cheshirecode-sandbox bash -lc '
  cd /workspace/projects/factory-brief
  WORKLOG_ROOT=/workspace/projects/_worklog WORKLOG_LDAP=fredtran npm run compile:knowledge
  npm test
'

# 4. Remote cheshirecode/* repos (clone fresh inside container):
bin/sandbox.sh test-repo <repo-name>       # = cheshirecode/<name>
bin/sandbox.sh test-repo <owner>/<repo>    # explicit owner

# 5. Interactive shell (npm install, commit, push — always inside container):
bin/sandbox.sh exec bash -l

# 6. When done — always tear down (borrow-only verify)
bin/sandbox.sh down                        # stops + removes container; volumes persist
# Agents: run `down` before ending the session if no other session needs the container.

# Reproducibility proof (also runs in CI):
bin/sandbox.sh nuke --all && bin/setup-from-scratch.sh

For multi-repo dogfooding (proven up to n=5+ repos in parallel), use sub-agents partitioned by repo name. But see Hazards § sub-agent-host-leak first.

Multi-session use + git sync

This repo is shared across Cursor / Claude / Codex sessions. That is safe when these rules hold:

Topic Safe pattern
Git history Merge commits on main are fine. Unlike _worklog, sandbox has no linear-history invariant. Sync with git pull (merge OK); avoid force-push unless coordinated maintenance.
Container bin/sandbox.sh up --no-attach reuses a running <login>-sandbox container — multiple sessions can docker exec into the same instance.
Cleanup Borrow-only verify (e.g. factory-brief npm run verify): run bin/sandbox.sh down when finished — do not leave <login>-sandbox running idle. Long-lived work inside the container is the exception; coordinate before down if another session may still docker exec.
Identity Every session runs source ~/Documents/oss/.envrc (or direnv) before up. See hazard #13.
Commits Mutating cheshirecode/* repos inside the sandbox via docker exec … git commit — never host-shell git commit. See hazard #12.
Teardown Only one session should run nuke / rebuild at a time; scoped to current $SANDBOX_LOGIN. down is per-session hygiene; nuke is full reset.
Workspace mounts OSS repos under $SANDBOX_WORKSPACE/workspace/oss; work repos under $SANDBOX_PROJECTS_DIR/workspace/projects. Each is a separate git repo — pull on host; sandbox-repo merges do not update them.

CI checks out HEAD only — merge depth on main does not affect ./tests/run.sh.

Conventions a coding agent must follow in this repo

D3 — Commit-trailer evidence

Every fix-shaped commit message ends with an Evidence: block citing the actual command output that proves the fix. Example:

Evidence (D3 trailer):
  $ ./tests/run.sh functional → 18 PASS / 0 FAIL (was 17; added #4j)
  $ docker exec ... sudo apt install -y nodejs npm → works
  $ bin/sandbox.sh test-repo frontend-ai-template → exit 0 (43/43 tests)

Past councils proved this convention is the cheapest auditing tool in the stack — it makes "did this fix actually work?" greppable in git log.

Karpathy voting bar (every new candidate item passes ALL 5)

When proposing a feature/refactor/fix, ask the 5 questions:

Criterion Pass test
TRACES Item directly addresses a user statement or filed issue
SOLVES-EXTANT-PAIN A real observed problem, not speculation
N-THRESHOLD-MET For abstractions: ≥3 concrete instances of the pattern
COST-PROPORTIONATE Implementation cost matches the asserted user value
NON-INFRA-PADDING User-visible, not tooling-for-future-tooling

If any fails: REJECT with cited criterion, or QUALIFY with a concrete trigger that would unblock it later. Full skill at ~/.claude/skills/council/SKILL.md.

Dogfood-as-definition-of-done

A change is not done until it's been run against a real cheshirecode/* repo end-to-end inside the sandbox. Structural tests catch shape; dogfood catches behavior. Every fix this session that landed had a dogfood pass.

Learned hazards (real bugs that bit; don't re-introduce)

These are surfaced in chronological order. Each was a real production bug that shipped despite ~18 structural tests passing.

  1. bash 3.2 incompatibility (mapfile) — macOS default /bin/bash is v3.2. Use inline read loops, not bash-4 builtins. CI must include a /bin/bash matrix row.

  2. set -u + empty arrayprintf '%s\n' "${args[@]}" aborts when args is empty under set -u. Inline the producer or guard with ${args[@]+"${args[@]}"}.

  3. docker-cp + --user dev chmod conflictdocker cp lands files as root; subsequent docker exec --user dev chmod fails. Use stdin pipe instead: printf '%s' "$x" | docker exec -i ... sh -c 'cat > /path'.

  4. tmpfs-mode=0700 + --user dev unwritable — root-mode tmpfs + dev writer = ENOENT. Use default tmpfs mode (1777, sticky world-rwx). Don't add tmpfs-mode without a threat model.

  5. Stale .gitconfig.lock aborts entrypoint — bind-mounted $HOME persists crash artifacts. Entrypoint must rm -f $HOME/.gitconfig.lock before git config --global calls. Test #4j.

  6. gh auth login --with-token rejects classic PATs missing read:org — login-time scope check too strict. Write ~/.config/gh/hosts.yml directly via gh api user probe (which works on minimal scopes).

  7. --security-opt=no-new-privileges blocks sudo apt — never add security flags without a named threat model. Removed; if re-added, must come with a documented attacker model.

  8. Entrypoint didn't wire git credential helpergh auth ≠ git push auth. Run gh auth setup-git in entrypoint.

  9. Named-volume mount points root-owned — Dockerfile must mkdir -p the mount targets (/workspace/home/.claude, .codex, etc.) as dev-owned BEFORE the volume initializes from the image dir. Otherwise the empty volume materializes root-owned. Test #4i.

  10. Node 18 in apt vs Node 22+ in modern vitestapt install nodejs on Ubuntu 24.04 gives Node 18; vitest 4 needs Node 22+ APIs (node:util.styleText). Bake NodeSource Node 20 LTS into image.

  11. shellcheck SC1087 $var[ ,] — bash sees array-index ambiguity. Brace: ${var}[ ,]. CI catches it; local pre-commit must too.

  12. Host-shell identity leak at up timebin/sandbox.sh up calls gh auth token on host, which returns whichever account the user is currently logged into. If the shell that ran up didn't direnv-load ~/Documents/oss/.envrc, the host's default gh login (often the work account) gets piped into the container. Entrypoint then derives git identity from that token via gh api user, baking the wrong identity into the running container — re-creating hazard #12 (work-identity leak) from a different vector. Observed: 4+ hours running as ideogram-fredtran instead of cheshirecode after a token rotation, until a docker exec ... git config user.email surfaced it. Fix in place (bin/sandbox.sh cmd_up, post-require_token): when $WORKLOG_LDAP, $SANDBOX_LOGIN_EXPECTED, OR a profile is explicitly selected (--profile= / $SANDBOX_PROFILE), the script probes gh api user .login with the about-to-be-piped token and refuses to start (exit 78) if it doesn't match the expected identity. Profile selection treats the profile's declared SANDBOX_LOGIN as the expected — auto-detected SANDBOX_LOGIN (no profile) is skipped to avoid a tautology. Bypass by unsetting whichever is set, or running without --profile=. Operational rule: always invoke as source ~/Documents/oss/.envrc && bin/sandbox.sh up (or direnv exec ~/Documents/oss …). The check is the safety net; direnv is the primary mechanism.

  13. Sub-agent host-leak when committing — a sub-agent told to "commit

    • push" can run git commit on the host (picking up the user's work git config) instead of via docker exec into the sandbox. The sandbox's identity isolation only applies INSIDE the container; it cannot constrain what sub-agents do on the host. Real leak observed this session: a sub-agent committed to a cheshirecode/* repo with a @ideogram.ai work email. Resolution: the affected repo was nuked from GitHub by the maintainer — the leaked commit no longer exists. Forward mitigation: every commit-mutating sub-agent prompt must explicitly say docker exec cheshirecode-sandbox bash -c "cd ...; git -c user.name=cheshirecode -c user.email=<id>+cheshirecode@users.noreply.github.com commit -m '...'" — never git commit directly in the agent's bash tool. Author-audit every push (gh api repos/<owner>/<repo>/commits --jq '.[].commit.author.email') before assuming the sandbox's identity isolation held end-to-end.
  14. OrbStack migration silently skips detached named volumesorbctl docker migrate (Docker Desktop → OrbStack) copies images and containers, plus volumes currently attached to a running container. Detached named volumes (<login>-toolchains, <login>-gh, <login>-claude, <login>-codex when the sandbox isn't up) are NOT copied, and the migrator doesn't warn. Observed on 2026-06-07: post-migration container started cleanly on OrbStack but the toolchains volume was empty — sandbox state silently reset. Fix: stream-copy each named volume between Docker contexts before relying on the migrator's output:

    docker --context=desktop-linux run --rm -v "$vol":/from alpine \
      tar -C /from -cf - . \
      | docker --context=orbstack run --rm -i -v "$vol":/to alpine \
      tar -C /to -xf -
    

    Verify with du -sh on both sides. The migrator itself remains useful for images and running containers — just don't trust it for detached state.

Subcommand reference

bin/sandbox.sh up                build (if needed) + run + drop into shell
bin/sandbox.sh up --no-attach    same but return after entrypoint runs
bin/sandbox.sh exec <cmd>        run cmd in the running container
bin/sandbox.sh run-headless <cmd> [args...]
                                 non-TTY run; writes stdout/stderr/exit/meta
                                 under learnings-inbox/headless-runs/
bin/sandbox.sh down              stop container; volumes preserved
bin/sandbox.sh rebuild           force rebuild image
bin/sandbox.sh doctor            host preflight + show detected layout
bin/sandbox.sh verify-llm-auth   in-container: do piped LLM creds work?
bin/sandbox.sh test-repo <name>  clone + install + test a cheshirecode/* repo
bin/sandbox.sh nuke [--all]      remove container + image + volumes
                                 (--all also removes .sandbox-home/, learnings-inbox/)

When the sandbox is the wrong tool

Use the sandbox for:

  • Working on cheshirecode/* repos (the design center)
  • Running Claude Code / Codex against personal-OSS code without work-creds risk
  • Exercising dependency installs that would pollute the host

Do NOT use the sandbox for:

  • Anything requiring the user's work identity (ideogram/*, work repos)
  • Long-running services (containers are designed ephemeral)
  • Cursor IDE-driven work — Cursor's auth is keychain-only and currently bound to the user's work account; documented out of scope
  • Workflows that need the in-container dev server reachable from the host browser (no port publishing in v1)

Pointers

  • DESIGN.md — original design council output (Anthropic auto-pipe v1)
  • README.md — user-facing quickstart
  • tests/run.sh — every named hazard above has a regression test
  • bin/setup-from-scratch.sh — reproducibility-loop entry point
  • bin/sandbox.sh test-repo — clone+install+test shortcut for cheshirecode/* repos
  • bin/sandbox.sh run-headless — daemon-safe non-TTY command runner with host-inspectable artifacts under learnings-inbox/headless-runs/

Council skill (orchestrates voting for any non-trivial proposal): ~/.claude/skills/council/SKILL.md (canonical) or ~/Documents/oss/dotfiles/skills/council/SKILL.md (vendored).