Skip to content

v2.7.0

Choose a tag to compare

@rappdw rappdw released this 01 Oct 16:59
6aa38dd

This release fixes the daemon sessions that parked on Claude Code's "Configuration error … .claude.json contains invalid JSON" dialog while --start reported ready (#400, also reported as #284). It adds sandy --rsync-from and sandy --approvals, extends SANDY_EFFORT to grok and gemini, and publishes a host-side path contract with new --print-state fields. schema_version stays 4: every introspection change is additive. SANDY_SANDBOX_MIN_COMPAT stays 2.0.0, so existing sandboxes keep working.

One-time rebuild. Agent images rebuild on first launch, because entrypoint.sh, user-setup.sh and the image labels changed. If that rebuild is deferred because the build mirrors are unreachable (#218), the launch warns and falls back to the 2.6 behaviour for the files described below until sandy --rebuild succeeds.

Fixed

#400: the .claude.json "Configuration error" dialog. The file on disk was never bad; a single read of it was. On macOS file sharing (measured on OrbStack), when a container has used a file and the host then replaces it, the next container's first read can come back short: the new bytes at the old length. Every read half a second later is fine. Sandy merged keys into .claude.json at every launch, so every relaunch set this up. test/spike/virtiofs-host-rewrite-spike.sh reproduces it with no sandy involved:

a container used the file, then… next container's first read fails
the host replaced it (in place or by rename) 16 of 60 relaunches
nothing touched it 0 of 20
the host wrote a fresh name, the container renamed it in (2.7.0) 0 of 60

The fix is a rule about who replaces the path: the host never replaces a path a container has used. Every per-launch change sandy makes in an agent home is written beside its target as <target>.sandy-launch.<id>. The container's user-setup.sh moves it into place before any agent starts, and a change that would leave the file identical is dropped. This covers claude/.claude.json, claude/settings.json, claude/statsig/, the cmux hook, gemini/settings.json and codex/config.toml. Files only the container reads (the session marker's mount source, composed agent args, the gemini effort file, the proxy config) get a fresh path per launch under <sandbox>/launch/<id>/.

#407: claude --continue refused for good. Claude Code's session registry kept records from earlier containers that it could never prove dead, so once the newest conversation had continued in a background session, --continue refused with "registered from another machine or container". The launch now prunes those records at the one point no container of the sandbox can exist.

#158: --doctor --fix could delete a stale lock that a new launch had just taken. It now uses the launch's atomic reap and re-checks at removal.

New

  • sandy --rsync-from <host> (#409) is the reverse of --rsync. It pulls the remote's sandbox for this workspace using only outbound ssh/rsync, which works where the other host cannot reach this one. A failed pull installs nothing.
  • sandy --approvals [--workspace P] (#296) reports, as one JSON document, whether each launch approval gate (privileged workspace keys, dangerous symlinks, the .sandy/Dockerfile build) is approved, pending, changed, refused or not_applicable. It is read-only: no grant path, no Docker. Exit 0 all clear, 2 something unresolved.
  • SANDY_EFFORT reaches grok and gemini (#116). Grok gets --reasoning-effort (max is clamped to xhigh, with a notice). Gemini gets a sandy-owned, read-only system settings file. Opencode is deliberately not wired.
  • Proxy identity (#299). The proxy image carries sandy.proxy_src/sandy.proxy_epoch labels. A launch whose proxy image does not match warns and proceeds, and --update-sessions now restarts a session whose proxy image is stale, not only its agent image.

What you might notice

  • .claude.json lives at <sandbox>/claude/.claude.json (#411), read through CLAUDE_CONFIG_DIR=/home/sandy/.claude. A pre-2.7 $SANDY_HOME/sandboxes/<name>.claude.json is moved in once at launch. A copy that reappears later is left untouched and warned about at every launch. A tool that wrote the old path must write the new one; find it from --print-state's sandboxes[].path.
  • claude/settings.json as seen from the host shows the previous launch's version until the container starts and installs the staged copy.
  • The .sandy/Dockerfile review shows the whole file, and the build context no longer includes .sandy/config or .sandy/.secrets (#295). A .sandy/ created during a session is flagged at the next approval prompt.
  • A proxy image built before 2.7.0 warns "no identity labels" until it rebuilds (the monthly epoch, or sandy --rebuild).

For consumers

  • Host-side path contract (#386). Only three things about a sandbox are a contract for host tools: the agent-home mapping (claude/ → ~/.claude/, and likewise for the other agents), the feature mount destinations, and the daemon container labels. Everything else under $SANDY_HOME is private and may change in any release, including launch/, the host copy of sandy-session.json, and the files sandy seeds.
  • New --print-state fields, all additive:
    • marker: the session marker's state, version and launch time (#386);
    • cross_session_inbound: the pinned value plus what the two settings files actually say (#386);
    • dockerfile_approvals (#296);
    • image: the image that actually ran, also in the session marker (#295);
    • full mode only: proxy_image_src, proxy_image_epoch (#299).
  • Writing into a sandbox from the host. The #400 mechanism applies to any host tool, not only sandy. A tool that replaces a file in an agent home, or a feature payload file, after a container has used it can be read short by the next container on macOS. Write a new name and switch to it, or write only while no container of that sandbox exists.

Testing

  • CI (Linux): the Tests workflow is green on the release commit.
  • On the maintainer's macOS host: run-tests.sh passes. The cross-session delivery harness (§25), the section this release's main fix targets, passed three consecutive runs (23/23 each). The spike results are above. run-integration-tests.sh was not re-run in full on the release commit; its last full run, on #406, was 90/91, the one failure being #400.
  • Known red: the scheduled Linux Integration workflow has failed one or two checks in the handoff-directories acceptance harness (§23) on GitHub runners since before 2.6.0. Its next scheduled run will be the first on this release's code.