This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git
The following commit(s) were added to refs/heads/main by this push:
new d1c8797 claude-iso: auto-allow current repo in sandbox on every
launch (#249)
d1c8797 is described below
commit d1c87970bb61c0f0ee37102b90e58e4a347d2f3a
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat May 23 01:58:29 2026 +0200
claude-iso: auto-allow current repo in sandbox on every launch (#249)
Whenever claude-iso is launched from inside a git working tree,
resolve `git rev-parse --show-toplevel` and prepend it to
sandbox.filesystem.allowRead via a one-shot `--settings` JSON.
This closes the visibility gap for the wrapper-launch path:
without it, the agent can't read the source tree the user just
`cd`'d into unless the path was hand-listed in
`<repo>/.claude/settings.local.json` ahead of time (the fix the
project-root coverage docs describe for plain `claude`).
The previous `-w`/`--worktree` injection is preserved as an
additive layer — when `-w` is on the argv and `$PWD` is a
worktree, the main repo is also added; in the main repo, the
toplevel and the main repo dedupe to a single entry. Both
paths ride into the session via one `--settings` injection.
Outside a git repo, this is a silent no-op. The stderr banner
now reports the full list of paths added.
The setup guide gains a short subsection covering both the new
current-repo auto-allow and the worktree mode (the latter
landed in #157 without a docs cross-ref).
Generated-by: Claude Code (Claude Opus 4.7)
---
docs/setup/secure-agent-setup.md | 36 ++++++++++++
tools/agent-isolation/claude-iso.sh | 106 ++++++++++++++++++++++++++----------
2 files changed, 113 insertions(+), 29 deletions(-)
diff --git a/docs/setup/secure-agent-setup.md b/docs/setup/secure-agent-setup.md
index ef60f96..c202d66 100644
--- a/docs/setup/secure-agent-setup.md
+++ b/docs/setup/secure-agent-setup.md
@@ -19,6 +19,7 @@
- [When the helper runs](#when-the-helper-runs)
- [Per-project vs whole-user scope](#per-project-vs-whole-user-scope)
- [The clean-env wrapper](#the-clean-env-wrapper)
+ - [Automatic sandbox allow-paths](#automatic-sandbox-allow-paths)
- [Sandbox-bypass visibility hook](#sandbox-bypass-visibility-hook)
- [Why install it user-scope, not
project-scope](#why-install-it-user-scope-not-project-scope)
- [Install (user-scope)](#install-user-scope)
@@ -776,6 +777,41 @@ CLAUDE_ISO_ALLOW="GH_TOKEN" GH_TOKEN="$(op read
'op://Personal/GitHub/token')" c
The `CLAUDE_ISO_ALLOW` mechanism is opt-in per invocation — no
implicit propagation, no persistent allowlist.
+### Automatic sandbox allow-paths
+
+Beyond the env-stripping role, `claude-iso` also injects up to two
+absolute paths into the session's `sandbox.filesystem.allowRead`
+via a one-shot `claude --settings <json>` flag prepended to the
+argv. The injection merges with the loaded settings stack at
+startup, *before* sandbox initialisation, so the paths take
+effect for that session immediately — no on-disk
+`settings.local.json` edit, no per-checkout bootstrap, nothing
+to clean up afterwards. A stderr banner reports what was added.
+
+**Current-repo auto-allow (always on).** Whenever `claude-iso` is
+launched from inside a git working tree, the working-tree root
+(resolved via `git rev-parse --show-toplevel`) is added to
+`allowRead`. This closes the visibility gap described in
+[Project-root coverage in the sandbox
allowlists](#project-root-coverage-in-the-sandbox-allowlists)
+for the wrapper-launch path: when launched through `claude-iso`,
+you do not also need the project root hand-listed in
+`<repo>/.claude/settings.local.json` for the agent to be able to
+read the source tree. (The settings.local.json fix remains the
+right answer for plain `claude` launches — the harness can't
+see the wrapper's argv.) Outside a git repo, this is a silent
+no-op.
+
+**Worktree mode (`claude-iso -w` / `claude-iso --worktree`).**
+Additive on top of the current-repo auto-allow. When `-w` is on
+the argv and `$PWD` is a worktree, the *main* repo (resolved via
+`git rev-parse --git-common-dir`) is also added — that path is
+otherwise unreachable from a worktree session, because the
+sandbox's relative `.` rule covers only the worktree itself.
+Run inside the main repo, `-w` is effectively a no-op: the
+working-tree root and the main repo resolve to the same path
+and dedupe into a single `allowRead` entry. Both paths ride
+into the session via a single `--settings` injection.
+
## Sandbox-bypass visibility hook
The Bash tool accepts a `dangerouslyDisableSandbox: true` flag that
diff --git a/tools/agent-isolation/claude-iso.sh
b/tools/agent-isolation/claude-iso.sh
index a663237..a726998 100755
--- a/tools/agent-isolation/claude-iso.sh
+++ b/tools/agent-isolation/claude-iso.sh
@@ -40,17 +40,29 @@
# GH_TOKEN="$(gh auth token)" claude-iso
# AWS_PROFILE=read-only claude-iso
#
+# Current-repo auto-allow:
+# Whenever the wrapper is invoked from inside a git working
+# tree, claude-iso automatically grants the session's sandbox
+# read access to that working tree's root (resolved via
+# `git rev-parse --show-toplevel`). Without this, the agent
+# can't read the source the user just `cd`'d into unless the
+# repo path was hand-listed in `.claude/settings.json` ahead of
+# time. Outside a git repo it's a silent no-op. The path is
+# injected via a one-shot `--settings` merge — nothing on disk
+# changes — and a stderr banner reports what was added.
+#
# Worktree mode (`claude-iso -w` / `claude-iso --worktree`):
-# When `-w` / `--worktree` is present in the args AND the wrapper
-# is invoked from inside a git repo, claude-iso automatically
-# grants the new worktree session's sandbox read access to the
-# *main* repo (resolved via `git rev-parse --git-common-dir`, so
-# it works whether you launch from the main checkout or from a
-# nested worktree). The wrapper prepends a one-shot
-# `--settings '{"sandbox":{"filesystem":{"allowRead":["<main-repo>"]}}}'`
-# to the `claude` argv — Claude merges this into the loaded
-# settings stack at startup, before the sandbox is initialised.
-# A stderr banner reports what was added. Nothing on disk changes.
+# Additive on top of the current-repo auto-allow above. When
+# `-w` / `--worktree` is present in the args AND the wrapper is
+# invoked from inside a git repo, claude-iso also grants read
+# access to the *main* repo (resolved via
+# `git rev-parse --git-common-dir`, so it works whether you
+# launch from the main checkout or from a nested worktree).
+# When run in the main repo, the toplevel and the main repo
+# resolve to the same path and are deduped. Both paths ride
+# into the session via a single `--settings` injection that
+# Claude merges into the loaded settings stack at startup,
+# before the sandbox is initialised.
claude_iso_main() {
# Resolve the claude binary on PATH before clobbering the env so
@@ -142,13 +154,25 @@ claude_iso_main() {
# without a shadow. The conservative read: include these only when
# the user named them in CLAUDE_ISO_ALLOW.)
- # `-w` / `--worktree`: auto-add the main repo to the new worktree
- # session's sandbox allowRead. See the "Worktree mode" section in
- # the file header for the full rationale. The injection uses
- # `claude --settings <json>`, which merges with the loaded settings
- # stack at startup (i.e. before sandbox init), so the added path is
- # in scope for the worktree session immediately — no on-disk
- # settings.json edit is performed.
+ # Sandbox auto-allow injection. See the "Current-repo auto-allow"
+ # and "Worktree mode" sections in the file header for the full
+ # rationale. The injection uses `claude --settings <json>`, which
+ # merges with the loaded settings stack at startup (i.e. before
+ # sandbox init), so the added paths are in scope for the session
+ # immediately — no on-disk settings.json edit is performed.
+ #
+ # We collect up to two candidate paths:
+ # - cwd_toplevel: the working tree root of $PWD (always when
+ # inside a git repo). Lets Claude read the source the user
+ # just `cd`'d into.
+ # - main_repo: the parent of the main repo's .git dir; added
+ # only when `-w`/`--worktree` is on the argv, so worktree
+ # sessions can see the original checkout.
+ # When both resolve to the same path (no worktree, or `-w` from
+ # the main repo) they collapse to a single entry.
+ local cwd_toplevel
+ cwd_toplevel="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null || true)"
+
local has_worktree=0
local arg
for arg in "$@"; do
@@ -157,8 +181,9 @@ claude_iso_main() {
esac
done
+ local main_repo=""
if [[ "$has_worktree" -eq 1 ]]; then
- local common_dir main_repo
+ local common_dir
common_dir="$(git -C "$PWD" rev-parse --git-common-dir 2>/dev/null ||
true)"
if [[ -n "$common_dir" ]]; then
case "$common_dir" in
@@ -166,18 +191,41 @@ claude_iso_main() {
*) common_dir="$PWD/$common_dir" ;;
esac
main_repo="$(cd "$(dirname "$common_dir")" 2>/dev/null && pwd)"
- if [[ -n "$main_repo" ]]; then
- # Escape backslashes and double quotes so a pathological
- # repo path can't break out of the JSON string literal.
- local escaped="${main_repo//\\/\\\\}"
- escaped="${escaped//\"/\\\"}"
- set -- --settings
"{\"sandbox\":{\"filesystem\":{\"allowRead\":[\"${escaped}\"]}}}" "$@"
- if [[ -t 2 ]]; then
- printf '\033[2m[claude-iso] -w detected; added main repo "%s" to
worktree sandbox allowRead\033[0m\n' "$main_repo" >&2
- else
- printf '[claude-iso] -w detected; added main repo "%s" to worktree
sandbox allowRead\n' "$main_repo" >&2
- fi
+ fi
+ fi
+
+ local -a allow_read_paths=()
+ local candidate existing seen
+ for candidate in "$cwd_toplevel" "$main_repo"; do
+ [[ -z "$candidate" ]] && continue
+ seen=0
+ for existing in "${allow_read_paths[@]}"; do
+ if [[ "$existing" == "$candidate" ]]; then
+ seen=1
+ break
fi
+ done
+ [[ "$seen" -eq 0 ]] && allow_read_paths+=("$candidate")
+ done
+
+ if (( ${#allow_read_paths[@]} > 0 )); then
+ # Hand-roll the JSON array literal (escape backslashes and
+ # double quotes) so a pathological repo path can't break out
+ # of the string literal. Keeping it dependency-free — no jq.
+ local json_array="" banner_paths="" sep=""
+ local p escaped
+ for p in "${allow_read_paths[@]}"; do
+ escaped="${p//\\/\\\\}"
+ escaped="${escaped//\"/\\\"}"
+ json_array+="${sep}\"${escaped}\""
+ banner_paths+="${sep}\"${p}\""
+ sep=","
+ done
+ set -- --settings
"{\"sandbox\":{\"filesystem\":{\"allowRead\":[${json_array}]}}}" "$@"
+ if [[ -t 2 ]]; then
+ printf '\033[2m[claude-iso] added to sandbox allowRead: %s\033[0m\n'
"$banner_paths" >&2
+ else
+ printf '[claude-iso] added to sandbox allowRead: %s\n' "$banner_paths"
>&2
fi
fi