diff --git a/secure-agent-setup.md b/secure-agent-setup.md index abeb35670..a90557d52 100644 --- a/secure-agent-setup.md +++ b/secure-agent-setup.md @@ -7,6 +7,7 @@ - [Three-layer defence](#three-layer-defence) - [Required tools (pinned versions)](#required-tools-pinned-versions) - [Install commands](#install-commands) + - [Distro-specific shortcut — Linux Mint 22.x / Ubuntu 24.04 Noble](#distro-specific-shortcut--linux-mint-22x--ubuntu-2404-noble) - [Bumping a pinned version](#bumping-a-pinned-version) - [Wiring the check script into a weekly routine](#wiring-the-check-script-into-a-weekly-routine) - [The framework's own `.claude/settings.json`](#the-frameworks-own-claudesettingsjson) @@ -347,6 +348,28 @@ The wrapper hard-allows only a tiny passthrough list (`HOME`, `PATH`, `USER`, `LOGNAME`, `PWD`); everything else from the parent shell is dropped via `env -i`. +**Optional — make the isolated wrapper your default `claude`.** Once +the wrapper is sourced, you can alias `claude` to it so every plain +`claude` invocation goes through the clean-env path: + +```bash +# in your ~/.bashrc or ~/.zshrc, *after* the source line above +alias claude='claude-iso' +``` + +The wrapper resolves the underlying binary via shell-aware path lookup +(`type -P` in bash, `whence -p` in zsh) rather than `command -v`, so +the alias does not loop back into itself. Each launch prints a dim +one-line banner on stderr (`[claude-iso] running in isolated env (…)`) +so it is obvious which mode the agent is starting in. To bypass the +alias for a single invocation, use `command claude …` or `\claude …`. + +The trade-off is the same one as any "shadow the binary with a safer +wrapper" pattern: a session you forgot to start in a tracker checkout +also runs with a stripped env, which surprises tools that rely on a +parent-shell credential. If that bites, drop the alias and call +`claude-iso` explicitly when you actually want the isolation. + To inject one credential explicitly for one session: ```bash diff --git a/tools/agent-isolation/claude-iso.sh b/tools/agent-isolation/claude-iso.sh index cab028531..8581513b1 100755 --- a/tools/agent-isolation/claude-iso.sh +++ b/tools/agent-isolation/claude-iso.sh @@ -42,9 +42,17 @@ claude_iso_main() { # Resolve the claude binary on PATH before clobbering the env so - # the lookup uses the user's normal $PATH. + # the lookup uses the user's normal $PATH. Use a path-only lookup + # (bash `type -P`, zsh `whence -p`) instead of `command -v`: with + # `command -v`, an `alias claude=claude-iso` in the user's rc file + # (a documented setup option — see `secure-agent-setup.md`) would + # resolve back to the alias and recurse. local claude_bin - claude_bin="$(command -v claude || true)" + if [[ -n "${ZSH_VERSION-}" ]]; then + claude_bin="$(whence -p claude 2>/dev/null || true)" + else + claude_bin="$(type -P claude 2>/dev/null || true)" + fi if [[ -z "$claude_bin" ]]; then echo "claude-iso: 'claude' not found on PATH. Install per secure-agent-setup.md." >&2 return 127 @@ -74,11 +82,15 @@ claude_iso_main() { ) # Build an `env -i ... NAME=value ...` argv from the passthrough list. + # Use `eval` for the indirect lookup so this works under both bash and + # zsh — bash's `${!var}` indirect expansion is a "bad substitution" in + # zsh. local -a env_args=() - local var + local var val for var in "${passthrough[@]}"; do - if [[ -n "${!var-}" ]]; then - env_args+=("${var}=${!var}") + eval "val=\${$var-}" + if [[ -n "$val" ]]; then + env_args+=("${var}=${val}") fi done @@ -94,9 +106,19 @@ claude_iso_main() { # in for one session via: # CLAUDE_ISO_ALLOW="GH_TOKEN AWS_PROFILE" GH_TOKEN=... claude-iso if [[ -n "${CLAUDE_ISO_ALLOW-}" ]]; then - for var in $CLAUDE_ISO_ALLOW; do - if [[ -n "${!var-}" ]]; then - env_args+=("${var}=${!var}") + # Word-split portably: zsh doesn't split unquoted parameters by default + # (it needs ${=var}), whereas bash does. Build an array either way. + local -a allow_list + if [[ -n "${ZSH_VERSION-}" ]]; then + allow_list=(${=CLAUDE_ISO_ALLOW}) + else + # shellcheck disable=SC2206 + allow_list=($CLAUDE_ISO_ALLOW) + fi + for var in "${allow_list[@]}"; do + eval "val=\${$var-}" + if [[ -n "$val" ]]; then + env_args+=("${var}=${val}") fi done fi @@ -108,6 +130,16 @@ claude_iso_main() { # without a shadow. The conservative read: include these only when # the user named them in CLAUDE_ISO_ALLOW.) + # When the user has aliased `claude=claude-iso`, an interactive + # session looks indistinguishable from a normal `claude` launch. + # Print a one-line banner on stderr (dim if a TTY) so it's obvious + # which mode the agent is starting in. + if [[ -t 2 ]]; then + printf '\033[2m[claude-iso] running in isolated env (%s)\033[0m\n' "$claude_bin" >&2 + else + printf '[claude-iso] running in isolated env (%s)\n' "$claude_bin" >&2 + fi + exec env -i "${env_args[@]}" "$claude_bin" "$@" }