This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch docs-secure-setup-bypass-hook
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git

commit 35bf73df0860ea3faa10968c2eb13e54b17662b9
Author: Jarek Potiuk <[email protected]>
AuthorDate: Thu Apr 30 11:38:02 2026 +0200

    docs(secure-agent-setup): sandbox-bypass warning hook + private-repo sync 
pattern
    
    Two new sections in `secure-agent-setup.md`:
    
    - "Sandbox-bypass visibility hook" — install + wiring for a
      PreToolUse hook that prints a bold-red banner whenever the
      model invokes Bash with `dangerouslyDisableSandbox: true`.
      Recommended user-scope (`~/.claude/settings.json`) so it fires
      in every session on the host, not just tracker sessions.
      Includes a Verify snippet and Trade-offs notes (visibility-not-
      block; grep-based JSON match for schema robustness).
    
    - "Syncing user-scope config across machines" — the dotfile-style
      pattern for keeping `CLAUDE.md`, hook scripts, and an optional
      global `claude-iso.sh` in lockstep across hosts via a private
      git repo. What-to-track table, repo layout, fresh-host setup,
      a minimal \`sync.sh\`, and a "Why a private repo" rationale.
    
    Also adds two recommended/optional steps to "Adopter setup"
    linking to the new sections.
    
    Ships the hook script as \`tools/agent-isolation/sandbox-bypass-warn.sh\`
    with the same ASF license header as the sibling \`claude-iso.sh\`,
    and adds it to the directory's README files table.
    
    The doctoc-managed TOC at the top of \`secure-agent-setup.md\` is
    NOT updated by this commit — re-run doctoc before merging.
    
    Generated-by: Claude Code (Claude Opus 4.7)
---
 secure-agent-setup.md                        | 243 +++++++++++++++++++++++++++
 tools/agent-isolation/README.md              |   1 +
 tools/agent-isolation/sandbox-bypass-warn.sh |  97 +++++++++++
 3 files changed, 341 insertions(+)

diff --git a/secure-agent-setup.md b/secure-agent-setup.md
index a90557d..f74bc02 100644
--- a/secure-agent-setup.md
+++ b/secure-agent-setup.md
@@ -383,6 +383,238 @@ 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.
 
+## Sandbox-bypass visibility hook
+
+The Bash tool accepts a `dangerouslyDisableSandbox: true` flag that
+lets the model run a single command outside the sandbox — necessary
+for the (rare) cases where a legitimate task needs to read or write
+a path that the sandbox denies. Claude Code prompts the user before
+honouring the bypass, but in a long session the prompt is easy to
+skim past, especially when several appear in quick succession.
+
+The framework ships a `PreToolUse` hook in
+[`tools/agent-isolation/sandbox-bypass-warn.sh`](tools/agent-isolation/sandbox-bypass-warn.sh)
+that makes every bypass attempt visually impossible to miss: a bold
+red banner with the command and the model's stated reason printed
+to stderr, before the permission prompt appears.
+
+The hook is **complementary** to the rest of the secure setup, not a
+replacement: it does not prevent a bypass, it just makes the bypass
+visible. The user still has to approve the call at the permission
+prompt — the banner gives them a fair chance to read what they are
+about to approve.
+
+### Why install it user-scope, not project-scope
+
+Unlike the framework's
+[`.claude/settings.json`](.claude/settings.json) (which is
+repo-scoped — only sessions started inside the tracker repo see
+it), this hook is most useful in
+**`~/.claude/settings.json`** — the user-scope config that applies
+to *every* Claude Code session on the host, tracker or otherwise.
+A sandbox-bypass attempt is just as worth noticing in an unrelated
+project as in the tracker.
+
+Per-project-scope installation is also valid (drop the same hook
+entry into a tracker's `.claude/settings.json`) — the trade-off is
+narrower coverage in exchange for one fewer file to manage at the
+user level.
+
+### Install (user-scope)
+
+```bash
+# Copy the hook script into ~/.claude/scripts/ (or symlink it from
+# the framework checkout — see "Syncing user-scope config across
+# machines" below for the multi-host pattern).
+mkdir -p ~/.claude/scripts
+cp /path/to/airflow-steward/tools/agent-isolation/sandbox-bypass-warn.sh \
+    ~/.claude/scripts/sandbox-bypass-warn.sh
+chmod +x ~/.claude/scripts/sandbox-bypass-warn.sh
+```
+
+Then wire the hook into `~/.claude/settings.json` under the
+`PreToolUse` block, matched on the `Bash` tool. If a `Bash` matcher
+already exists (e.g. for an unrelated hook), append to its `hooks`
+array rather than creating a second matcher block:
+
+```jsonc
+{
+  "hooks": {
+    "PreToolUse": [
+      {
+        "matcher": "Bash",
+        "hooks": [
+          {
+            "type": "command",
+            "command": "~/.claude/scripts/sandbox-bypass-warn.sh"
+          }
+        ]
+      }
+    ]
+  }
+}
+```
+
+### Verify
+
+The hook is exit-code-driven — exit 1 with stderr output means
+"show stderr to the user, tool proceeds". To test without a real
+bypass:
+
+```bash
+echo '{"tool_name":"Bash","tool_input":{"command":"ls 
~/.aws","description":"check aws creds","dangerouslyDisableSandbox":true}}' \
+    | ~/.claude/scripts/sandbox-bypass-warn.sh; echo "exit=$?"
+```
+
+Expected: a four-line red banner on stderr, then `exit=1`. A second
+call with `dangerouslyDisableSandbox` set to `false` (or absent
+entirely) should produce no output and `exit=0`.
+
+### Trade-offs
+
+- **No block, only visibility.** The hook deliberately exits 1, not
+  2 — exit 2 would block the call outright, and that defeats the
+  model's ability to do legitimate work the user has just asked for
+  (e.g. installing packages outside the project tree). If a stricter
+  posture is wanted, change the script's `exit 1` to `exit 2`; the
+  consequence is that *every* sandbox-bypass attempt then has to be
+  unblocked by editing the hook out, which in practice trains the
+  user to skip the safety entirely. Visibility-with-prompt is the
+  better steady state.
+- **Schema robustness.** The hook greps the JSON payload for
+  `"dangerouslyDisableSandbox": true` rather than reading a fixed
+  JSON path via `jq`, so it keeps working if Claude Code reshuffles
+  where in the payload the flag lives. Cost: a future Claude Code
+  release that renames the flag will silently stop firing the hook
+  until the regex is updated. Re-run the verification snippet after
+  every Claude Code upgrade — same cadence as the
+  [Verification](#verification) section below.
+
+## Syncing user-scope config across machines
+
+The user-scope pieces of the secure setup —
+`~/.claude/scripts/sandbox-bypass-warn.sh`, an optional global copy
+of `claude-iso.sh` (per the
+[Global (user-scope) install](#the-clean-env-wrapper) trade-off),
+your personal `~/.claude/CLAUDE.md`, plus any other custom hooks —
+only protect a host once they are installed there. Working on more
+than one machine means keeping all of them in lockstep, by hand,
+forever. That is exactly the workflow a small dotfile-style sync
+repo solves.
+
+The recommended pattern is a **private** git repository (private,
+not public, because `~/.claude/CLAUDE.md` typically carries personal
+collaboration preferences and the scripts may reference internal
+paths). Track the artifacts you want shared, symlink them into
+`~/.claude/`, and run a small sync script that pulls/commits/pushes.
+
+### What to track, what not to track
+
+| Track in the synced repo | Keep per-machine |
+|---|---|
+| `CLAUDE.md` (personal collaboration prefs) | `~/.claude/.credentials.json` — 
⚠ secret, never commit |
+| `scripts/sandbox-bypass-warn.sh` and any other hooks | 
`~/.claude/sessions/`, `~/.claude/history.jsonl` — session state |
+| `agent-isolation/claude-iso.sh` (if you globally installed it per the 
wrapper section) | `~/.claude/projects/` — per-project memory and tasks |
+| Custom slash commands (`commands/<name>.md`) | `~/.claude/settings.json` — 
typically differs per host (plugins, statusLine paths, voice) |
+| MCP servers you've audited and want everywhere (`.mcp.json` shape, by hand) 
| `~/.claude/settings.local.json` — by design machine-specific |
+
+The settings.json line is worth highlighting: it is tempting to
+sync it, and it does work, but in practice the machines drift
+(different plugin sets, different terminal capabilities) and the
+last-writer-wins behaviour of a naive sync script overwrites the
+divergent settings every push. Keep it per-machine and document
+the **wiring** instead — i.e. ship the `scripts/` directory in the
+synced repo, then on each new host edit `~/.claude/settings.json`
+once to point at the synced scripts. The "Install" snippets above
+already follow this pattern.
+
+### Layout
+
+A minimal repo layout:
+
+```
+~/.claude-config/                       # the synced repo's checkout
+├── CLAUDE.md                           # symlinked → ~/.claude/CLAUDE.md
+├── scripts/
+│   └── sandbox-bypass-warn.sh          # symlinked → 
~/.claude/scripts/sandbox-bypass-warn.sh
+├── agent-isolation/
+│   └── claude-iso.sh                   # symlinked → 
~/.claude/agent-isolation/claude-iso.sh
+├── README.md                           # what's in the repo, install steps 
per machine
+└── sync.sh                             # the pull/commit/push helper
+```
+
+Each tracked artifact lives in the repo; the path under `~/.claude/`
+is a symlink pointing at the repo. Editing either side updates both.
+
+### Setting up a fresh host
+
+```sh
+git clone [email protected]:<you>/claude-config.git ~/.claude-config
+
+# CLAUDE.md
+mkdir -p ~/.claude
+[ -f ~/.claude/CLAUDE.md ] && [ ! -L ~/.claude/CLAUDE.md ] && \
+    mv ~/.claude/CLAUDE.md ~/.claude/CLAUDE.md.bak
+ln -sf ~/.claude-config/CLAUDE.md ~/.claude/CLAUDE.md
+
+# Sandbox-bypass warning hook
+mkdir -p ~/.claude/scripts
+ln -sfn ~/.claude-config/scripts/sandbox-bypass-warn.sh \
+    ~/.claude/scripts/sandbox-bypass-warn.sh
+
+# (Optional) global claude-iso wrapper — see the wrapper section
+mkdir -p ~/.claude/agent-isolation
+ln -sfn ~/.claude-config/agent-isolation/claude-iso.sh \
+    ~/.claude/agent-isolation/claude-iso.sh
+```
+
+Then wire the per-machine bits one time, per the install snippets
+in the relevant sections (the hook entry in
+`~/.claude/settings.json`, the `source …/claude-iso.sh` line in
+`~/.bashrc` / `~/.zshrc`, etc.).
+
+### A minimal `sync.sh`
+
+The script is intentionally tiny — pull, commit anything dirty,
+push. Run it manually, on a cron, on a systemd timer, or wherever
+fits your workflow:
+
+```bash
+#!/usr/bin/env bash
+# Pull-commit-push the personal claude-config repo. Safe to run on
+# a timer: flock prevents concurrent runs, --rebase --autostash
+# carries any local edits through cleanly.
+set -u
+REPO="$HOME/.claude-config"
+LOCK="$REPO/.sync.lock"
+exec 9>"$LOCK"; flock -n 9 || exit 0
+cd "$REPO" || exit 1
+git pull --rebase --autostash
+git add -A
+git diff --cached --quiet || \
+    git commit -m "auto-sync from $(hostname) at $(date -Iseconds)"
+git log @{u}.. --oneline | grep -q . && git push
+```
+
+### Why a *private* repo
+
+Three reasons make this non-negotiable:
+
+1. **`CLAUDE.md` carries personal preferences.** Tone overrides
+   for specific people, opinions about review style, names of
+   internal projects — content you do not want indexed by GitHub
+   search.
+2. **Hooks may embed internal paths.** A custom statusline script
+   that pokes at `~/work/<employer>/` is not something to publish.
+3. **Audit surface for prompt-injection.** If the synced repo is
+   public and writable by anyone with a PR, an attacker can land
+   a malicious script that every host pulling the repo will then
+   execute on the next sync. A private repo with branch protection
+   (or a single-author push policy) closes that vector.
+
+Public dotfile repos are fine for shell aliases and editor configs;
+they are the wrong shape for agent-runtime files.
+
 ## Adopter setup
 
 If you are adopting the framework into your own tracker repo, copy
@@ -412,6 +644,17 @@ the secure setup into your tracker's working tree:
 4. Decide whether to gitignore `.claude/settings.local.json` in your
    tracker repo — Claude Code does this by default; verify with
    `git check-ignore .claude/settings.local.json`.
+5. **Recommended (user-scope, not repo-scope):** install the
+   sandbox-bypass warning hook per
+   [Sandbox-bypass visibility hook](#sandbox-bypass-visibility-hook).
+   It applies to every Claude Code session on the host (not only
+   tracker sessions), so it belongs in your user-scope
+   `~/.claude/settings.json` — not in the tracker's
+   `.claude/settings.json`.
+6. **Optional (multi-machine workflow):** keep the user-scope
+   pieces (the hook script, your personal `CLAUDE.md`, an optional
+   global `claude-iso.sh`) in a private dotfile-style repo per
+   [Syncing user-scope config across 
machines](#syncing-user-scope-config-across-machines).
 
 ## Verification
 
diff --git a/tools/agent-isolation/README.md b/tools/agent-isolation/README.md
index ba6711d..d0afba7 100644
--- a/tools/agent-isolation/README.md
+++ b/tools/agent-isolation/README.md
@@ -28,6 +28,7 @@ versions.
 | [`pinned-versions.toml`](pinned-versions.toml) | Machine-readable manifest 
of pinned upstream versions for `bubblewrap`, `socat`, and `claude-code`. Each 
entry carries a `released` date that satisfies the framework's 7-day cooldown 
convention. |
 | [`check-tool-updates.sh`](check-tool-updates.sh) | Reads the manifest and 
reports upstream releases that are newer than the pin AND have themselves aged 
past the 7-day cooldown. Side-effect-free — no installs, no edits, no PRs. |
 | [`claude-iso.sh`](claude-iso.sh) | Shell function to launch Claude Code with 
`env -i` and a tiny passthrough list, stripping every credential-shaped 
environment variable from the parent shell. The framework's "layer 0" of the 
secure setup. |
+| [`sandbox-bypass-warn.sh`](sandbox-bypass-warn.sh) | Claude Code 
`PreToolUse` hook (Bash matcher). Prints a bold-red banner to stderr whenever 
the model invokes the Bash tool with `dangerouslyDisableSandbox: true`. 
Belt-and-braces visibility for the sandbox-bypass permission prompt. 
Recommended user-scope (`~/.claude/settings.json`) so it fires across every 
session on the host. |
 
 ## Usage at a glance
 
diff --git a/tools/agent-isolation/sandbox-bypass-warn.sh 
b/tools/agent-isolation/sandbox-bypass-warn.sh
new file mode 100755
index 0000000..1cc54ba
--- /dev/null
+++ b/tools/agent-isolation/sandbox-bypass-warn.sh
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# sandbox-bypass-warn.sh — Claude Code PreToolUse hook (Bash matcher).
+#
+# Loudly warns whenever the model invokes the Bash tool with
+# `dangerouslyDisableSandbox: true` — i.e. when the model is asking
+# to leave Claude Code's filesystem/network sandbox for one call.
+#
+# Claude Code already prompts the user before honouring a sandbox
+# bypass, but the prompt is easy to skim past in long sessions.
+# This hook makes every bypass attempt visually impossible to miss:
+# a bold-red banner with the command and the model's stated reason.
+#
+# Recommended placement: user-scope `~/.claude/settings.json`, so
+# the warning fires for *every* Claude Code session on the host —
+# not only sessions inside a tracker repo whose project-level
+# `.claude/settings.json` would otherwise have to wire it itself.
+#
+# Behaviour:
+# - Reads tool input as JSON on stdin.
+# - If `"dangerouslyDisableSandbox": true` appears anywhere in the
+#   payload (matched as a JSON field, robust to schema changes
+#   across Claude Code versions), prints a bold-red banner to
+#   stderr with the command and the model's stated reason.
+# - Exits 1: a non-zero exit code that is *not* 2 means stderr is
+#   shown to the user in the terminal and the tool call still
+#   proceeds through the normal permission prompt. We want
+#   visibility, not block.
+# - Exit 2 would block the call entirely — *not* what we want; the
+#   user will be asked to authorise the bypass anyway, and a hard
+#   block here would defeat the model's ability to do work the
+#   user explicitly asked for.
+# - Otherwise exits 0 silently.
+#
+# Wiring (user-scope, applies to every session on the host):
+#
+#   {
+#     "hooks": {
+#       "PreToolUse": [
+#         {
+#           "matcher": "Bash",
+#           "hooks": [
+#             { "type": "command",
+#               "command": "~/.claude/scripts/sandbox-bypass-warn.sh" }
+#           ]
+#         }
+#       ]
+#     }
+#   }
+#
+# See `secure-agent-setup.md` → "Sandbox-bypass visibility hook"
+# for the install steps and the "Syncing user-scope config across
+# machines" section for the private-repo pattern that keeps this
+# hook in lockstep across hosts.
+
+set -u
+
+input=$(cat)
+
+# Match the JSON field anywhere in the payload. This is intentionally
+# more permissive than `jq -r '.tool_input.dangerouslyDisableSandbox'`
+# so the hook keeps working if Claude Code ever reshuffles where in
+# the payload the flag lives.
+if printf '%s' "$input" | grep -qE 
'"dangerouslyDisableSandbox"[[:space:]]*:[[:space:]]*true'; then
+  cmd=$(printf '%s' "$input"  | jq -r '.tool_input.command // ""'     
2>/dev/null || true)
+  desc=$(printf '%s' "$input" | jq -r '.tool_input.description // ""' 
2>/dev/null || true)
+
+  esc=$(printf '\033')
+  red="${esc}[1;31m"
+  reset="${esc}[0m"
+
+  {
+    printf '%s\041\041\041 SANDBOX BYPASS REQUESTED \041\041\041%s\n' "$red" 
"$reset"
+    [ -n "$desc" ] && printf '%sReason :%s %s\n' "$red" "$reset" "$desc"
+    [ -n "$cmd"  ] && printf '%sCommand:%s %s\n' "$red" "$reset" "$cmd"
+    printf '%sThis call will run OUTSIDE the sandbox.%s\n' "$red" "$reset"
+  } >&2
+  exit 1
+fi
+
+exit 0

Reply via email to