fix: keep secrets out of the podman argv, add --dry-run #24
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/secret-argv-and-dry-run"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
claude.shbuilt-e KEY=valuearguments for every variable in.env.claude,~/.claude-jail/env.default, andANTHROPIC_API_KEY. Process arguments areworld-readable on Linux, so any user on the host could read the API key for the
whole life of the container:
Verified before the fix: with a two-line
.env.claude, the key appeared in theargv twice — once from the env-file loop, once from the explicit API-key line.
What changed
1. Env vars are forwarded by name only.
-e KEYinstead of-e KEY=value.Podman reads each value from the environment it inherits from the wrapper, so
values never reach the command line. Wrapper-controlled variables whose container
value deliberately differs from the host's (
SSH_AUTH_SOCK=/ssh-agent,CLAUDE_CODE_WORKSPACE,TERM,COLUMNS,LINES) stay inline — name-only forSSH_AUTH_SOCKwould forward the host socket path and break agent forwarding.A guard warns and skips when an env-file key collides with one of the wrapper's
own internal variable names, so a stray
max_memory=…can't quietly leak thewrapper's internals into the container.
2.
--dry-run. Prints the exactpodman runinvocation and exits withoutrunning it. The command is assembled into a single array that is either printed
or
exec'd, so the printed command can't drift from the real one. No sessiondirectory is created; diagnostics go to stderr, so stdout is exactly the command.
This composes with the change above: because argv now carries only variable
names, the printed command contains no secrets and needs no redaction.
3. Fixes a pre-existing silent-exit bug. In
_load_env_file,[[ -z "${!key:-}" ]] && export …was the last command in awhilebody. Awhileloop returns its body's last status, so when the key was already set that&&returned 1, the function returned 1, andset -euo pipefailkilled thescript with no output at all.
Trigger: export a variable in your shell and list it last in
.env.claude. Atrailing comment or blank line masks it, which is likely why it went unnoticed.
.env.claude, withANTHROPIC_API_KEYexported in your shellMY_VAR=ok/ANTHROPIC_API_KEY=sk-from-file# commentANTHROPIC_API_KEY=sk-from-file/MY_VAR=ok4. Adds the project's first test suite — 26 tests, no new dependencies (no
bats, no shunit2). A stub
podmanonPATHrecords both the argv it received andthe environment it inherited, which is what makes the security property directly
assertable: the secret must be absent from argv and present in the environment.
Run with
make test.Behaviour changes worth knowing
.env.claude, the container now receives the shell value. Previously thefile's value leaked through even though the wrapper itself used the shell's.
The new behaviour matches the precedence the README already documented.
Session:and.claudeignorediagnostics now go to stderr, soclaude --dry-run . > cmd.shyields a clean file.Scope
Threat model is hiding secrets from other users on the host. Secrets remaining
in
.env.claudeplaintext on disk, and in/proc/<pid>/environ(readable only byyou and root), is unchanged and out of scope — as are at-rest encryption and
secret-manager integration.
Verification
End-to-end, against a real
.env.claude:SSH_AUTH_SOCK/TERMinline--dry-runstdoutNote on the plan document
docs/superpowers/plans/2026-07-27-secret-argv-and-dry-run.mdis the design docthis work was built from, committed for traceability. Drop those two
docs(plan)commits if you'd rather not carry it in the repo.
bash's `cd ""` returns 0 and stays in the current directory, so if `mktemp -d` failed, `TMP_ROOT="$(cd "$(mktemp -d)" && pwd)"` silently became the directory the suite was invoked from — the repo root. The old teardown's `[[ -n "$TMP_ROOT" ]]` guard then passed and `rm -rf "$TMP_ROOT"` deleted the working tree. setup() now checks mktemp before use, and teardown() refuses to remove anything outside ${TMPDIR:-/tmp}. tests/test_helpers_safety.sh covers the guard with a canary outside the temp root. Found by the final whole-branch review. Claude-Session: https://claude.ai/code/session_01PB8GPWDXX7pFnTFC4QZCYv