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.
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.
Every change must preserve these. Tests enforce them; the entrypoint refuses to start if they're violated.
-
No work identity in the container, ever. Entrypoint refuses to start if
$GITHUB_TOKEN,$GH_ENTERPRISE_TOKEN, or any var matching$SANDBOX_REFUSE_PATTERNSis set. Test #1/#2 intests/run.sh. -
Credentials transit via tmpfs
/run/secrets/*, never via-e,--build-arg, ordocker inspect-visible state. Entrypoint reads thenshred -us. Test #4b/#4e. -
HTTPS-only git remotes inside the container. SSH-agent forwarding is never enabled — it would tunnel the user's work SSH key. Test #5.
-
gpgsign=falseinside container. Signing on the host, not inside the sandbox. Test #7. -
gh auth setup-gitruns in entrypoint sogit push https://github.com/...doesn't prompt for username. Dogfood-discovered (see "Hazards" § credential-handling). -
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 devblind-spot).
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.shFor 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.
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.
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.
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.
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.
These are surfaced in chronological order. Each was a real production bug that shipped despite ~18 structural tests passing.
-
bash 3.2 incompatibility (
mapfile) — macOS default/bin/bashis v3.2. Use inline read loops, not bash-4 builtins. CI must include a/bin/bashmatrix row. -
set -u+ empty array —printf '%s\n' "${args[@]}"aborts whenargsis empty underset -u. Inline the producer or guard with${args[@]+"${args[@]}"}. -
docker-cp +
--user devchmod conflict —docker cplands files as root; subsequentdocker exec --user dev chmodfails. Use stdin pipe instead:printf '%s' "$x" | docker exec -i ... sh -c 'cat > /path'. -
tmpfs-mode=0700+--user devunwritable — root-mode tmpfs + dev writer = ENOENT. Use default tmpfs mode (1777, sticky world-rwx). Don't addtmpfs-modewithout a threat model. -
Stale
.gitconfig.lockaborts entrypoint — bind-mounted$HOMEpersists crash artifacts. Entrypoint mustrm -f $HOME/.gitconfig.lockbeforegit config --globalcalls. Test #4j. -
gh auth login --with-tokenrejects classic PATs missingread:org— login-time scope check too strict. Write~/.config/gh/hosts.ymldirectly viagh api userprobe (which works on minimal scopes). -
--security-opt=no-new-privilegesblockssudo apt— never add security flags without a named threat model. Removed; if re-added, must come with a documented attacker model. -
Entrypoint didn't wire git credential helper —
ghauth ≠git pushauth. Rungh auth setup-gitin entrypoint. -
Named-volume mount points root-owned — Dockerfile must
mkdir -pthe mount targets (/workspace/home/.claude,.codex, etc.) asdev-owned BEFORE the volume initializes from the image dir. Otherwise the empty volume materializes root-owned. Test #4i. -
Node 18 in apt vs Node 22+ in modern vitest —
apt install nodejson Ubuntu 24.04 gives Node 18; vitest 4 needs Node 22+ APIs (node:util.styleText). Bake NodeSource Node 20 LTS into image. -
shellcheck SC1087
$var[ ,]— bash sees array-index ambiguity. Brace:${var}[ ,]. CI catches it; local pre-commit must too. -
Host-shell identity leak at
uptime —bin/sandbox.sh upcallsgh auth tokenon host, which returns whichever account the user is currently logged into. If the shell that ranupdidn'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 viagh 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 asideogram-fredtraninstead ofcheshirecodeafter a token rotation, until adocker exec ... git config user.emailsurfaced it. Fix in place (bin/sandbox.shcmd_up, post-require_token): when$WORKLOG_LDAP,$SANDBOX_LOGIN_EXPECTED, OR a profile is explicitly selected (--profile=/$SANDBOX_PROFILE), the script probesgh api user .loginwith 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 declaredSANDBOX_LOGINas the expected — auto-detectedSANDBOX_LOGIN(no profile) is skipped to avoid a tautology. Bypass by unsetting whichever is set, or running without--profile=. Operational rule: always invoke assource ~/Documents/oss/.envrc && bin/sandbox.sh up(ordirenv exec ~/Documents/oss …). The check is the safety net; direnv is the primary mechanism. -
Sub-agent host-leak when committing — a sub-agent told to "commit
- push" can run
git commiton the host (picking up the user's work git config) instead of viadocker execinto 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 acheshirecode/*repo with a@ideogram.aiwork 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 saydocker exec cheshirecode-sandbox bash -c "cd ...; git -c user.name=cheshirecode -c user.email=<id>+cheshirecode@users.noreply.github.com commit -m '...'"— nevergit commitdirectly 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.
- push" can run
-
OrbStack migration silently skips detached named volumes —
orbctl 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>-codexwhen 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 -shon both sides. The migrator itself remains useful for images and running containers — just don't trust it for detached state.
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/)
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)
DESIGN.md— original design council output (Anthropic auto-pipe v1)README.md— user-facing quickstarttests/run.sh— every named hazard above has a regression testbin/setup-from-scratch.sh— reproducibility-loop entry pointbin/sandbox.sh test-repo— clone+install+test shortcut forcheshirecode/*reposbin/sandbox.sh run-headless— daemon-safe non-TTY command runner with host-inspectable artifacts underlearnings-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).