Skip to content

1.2.1: version bump + daemon/git/security fixes - #49

Merged
rappdw merged 6 commits into
mainfrom
bump/1.2.1-dev
Jul 17, 2026
Merged

1.2.1: version bump + daemon/git/security fixes#49
rappdw merged 6 commits into
mainfrom
bump/1.2.1-dev

Conversation

@rappdw

@rappdw rappdw commented Jul 16, 2026

Copy link
Copy Markdown
Owner

The 1.2.1 batch — version bump + the fixes found exercising 1.2.0 in real use. Five commits:

  • ea8bad0 — bump SANDY_VERSION 1.2.0 → 1.2.1-dev.
  • 2c49fb4security: redact secret VALUES in the passive-privileged approval prompt + approval file (they leaked credentials to the console and the on-disk daemon supervisor log).
  • e9ea1f7git auth: guaranteed-baseline credential helper independent of gh, so container git auth survives the transient api.github.com failures (api.github.com returns 503 on authenticated requests through the egress proxy (in-container) #50) that were silently breaking gh auth setup-git.
  • dcefe2edaemon approval: sandy --start grants passive-privileged approval on the client TTY before forking the non-TTY supervisor, so workspace .sandy/.secrets privileged keys are no longer silently dropped in daemon mode.
  • 5048899daemon zombie (daemon session zombies on agent exit; --attach returns exit 5 instead of session-ended #47): the supervisor now watches the inner session and tears the container down when the agent exits (ctrl-d/exit/crash), instead of leaving a zombie holding the proxy/networks/lock. docker rm -f prevents --restart unless-stopped from resurrecting it; reboot survival unchanged.

Verified statically + (git helper) end-to-end; the daemon-approval and stop-on-exit lifecycles are host gates. schema_version unchanged. Fast-forward main after CI. Closes #47.

🤖 Generated with Claude Code

rappdw and others added 4 commits July 16, 2026 23:01
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tput + file

The workspace-config privileged-key approval printed each pending key as
KEY=VALUE to stderr, and persisted the same KEY=VALUE set to the approval
file. For credential keys (CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY, …) that
leaked the full secret VALUE — to the console, and in daemon mode to the
on-disk supervisor log (~/.sandy/sandboxes/.daemon-start-*.log). The approval
file also stored the plaintext values needlessly (only its line-1 set_hash is
ever read back).

Both now emit KEY NAMES ONLY (`${_kv%%=*}`). The set_hash still encodes the
values, so editing a value still re-prompts. Existing approval files keep
matching (only line 1 matters).

Refs: daemon-mode passive-privileged approval (1.2.1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntainer git auth)

Container-side git auth relied entirely on `gh auth login --with-token` +
`gh auth setup-git`, both of which validate the forwarded token over the
network against api.github.com and swallow every error (2>/dev/null || true).
When api.github.com returns a transient failure (observed live: authenticated
api.github.com calls returning 503 while github.com and general egress are
fine — exposed now that the base image rebuilt with gh 2.96.0, which validates
during login where older gh just stored the token), the setup silently no-ops:
no credential.helper is configured and git push/pull fails, even though a
valid GIT_TOKEN was forwarded and present in the container env.

Append a direct credential helper as a guaranteed fallback: git tries helpers
in order, so gh's multi-account helper still wins when healthy, but this one
supplies the forwarded token for github.com with NO network round-trip. Reads
$GIT_TOKEN from the container env at call time (the literal ${GIT_TOKEN} is
stored in .gitconfig, never the value). Verified end-to-end: an authenticated
`git push --dry-run` succeeds via this helper alone with gh absent.

Template mirror regenerated.

Refs: container git-auth robustness (1.2.1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the client TTY

Daemon --start forks a non-TTY supervisor (stdin=/dev/null via nohup), so the
supervisor's passive-privileged approval could only ever fail closed — silently
dropping workspace-config privileged keys, including a credential in a
workspace .sandy/.secrets (the reported CLAUDE_CODE_OAUTH_TOKEN drop). There was
no way to approve in the daemon flow.

Before forking, the --start client now resolves the approval with its OWN TTY:
a synchronous subprocess run from the workspace in the new env-only
SANDY_APPROVE_ONLY mode reuses the full config-load + approval machinery,
prompts on the terminal, and persists the approval file — then exits (it stops
right after _resolve_passive_privileged_approval, before the workspace mutex,
the bare-sandy busy-gate, image builds, and launch, so it touches nothing but
the approval file and can't deadlock the lock or double-launch). The real
supervisor then re-loads and honors the persisted approval.

Gated on the client actually having a TTY (`[ -t 0 ] && [ -t 2 ]`); a non-TTY
client (sandy-ui / scripted) still can't prompt, so there approval must be
pre-granted (run `sandy --start` once from a terminal) or
SANDY_AUTO_APPROVE_PRIVILEGED set. Verified statically (bash -n; APPROVE_ONLY
exits before mutex/launch; subprocess before fork; no re-entrancy). The
end-to-end prompt/persist/honor cycle is a host gate (no Docker here).

Refs: daemon-mode passive-privileged approval (1.2.1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rappdw rappdw changed the title release: bump to 1.2.1-dev post-v1.2.0 1.2.1: version bump + daemon/git/security fixes Jul 17, 2026
rappdw and others added 2 commits July 17, 2026 00:39
…re zombie) (#47)

Daemon PID 1 is `tail -f /dev/null`, which never exits — so when the agent
itself ends (ctrl-d / /exit / crash) the tmux session dies but the container
stays Up, holding the egress proxy, per-session networks, and the workspace
lock, and making every later --attach return the confusing exit-5 ("container
still running, but tmux session probe failed"). Reported repeatedly in daemon
use; it also blocked credential refresh (a stale container never recycled).

The supervisor's block-and-wait loop now also WATCHES the inner session: it
polls `docker exec -u <uid> tmux has-session -t sandy` every 30s and, when the
session is gone for two consecutive polls while the container is still running
(the two-strike guard avoids tearing down on a transient docker-exec hiccup),
breaks out and exits — firing the EXIT trap → cleanup()'s `docker rm -f` +
network/lock teardown, exactly what --stop does. `docker rm -f` removes the
container outright, so `--restart unless-stopped` does NOT resurrect it (only a
bare exit would). --stop's SIGTERM still tears down via the trap as before, and
reboot survival (D9) is unchanged: a rebooted container has no supervisor
watching it and is handled by --stop/--attach via container-as-truth.

Single-location change (the supervisor loop); the container-side PID 1 is
untouched, so client-disconnect survival is preserved — only an AGENT exit now
ends the session. Verified statically; the real stop-on-exit lifecycle is a
host gate.

Refs #47

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he credential helper

The credential-helper string added in e9ea1f7 keeps ${GIT_TOKEN} literal on
purpose — it's stored verbatim in .gitconfig and expanded by the helper's own
shell at git-call time, so the value never lands in the config file. shellcheck
flags exactly that with SC2016; the run-tests.sh template shellcheck gate (§48)
failed on it (1/676). Add `# shellcheck disable=SC2016` in the heredoc source
with a rationale comment, and regenerate templates/user-setup.sh.tmpl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rappdw
rappdw merged commit 6a5f731 into main Jul 17, 2026
2 checks passed
@rappdw
rappdw deleted the bump/1.2.1-dev branch July 17, 2026 01:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

daemon session zombies on agent exit; --attach returns exit 5 instead of session-ended

1 participant