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 15be87f  docs(secure-agent-setup): sandbox-bypass warning hook + 
private-repo sync pattern (#17)
15be87f is described below

commit 15be87f9fda3668acad98165614ac5df0cb585b7
Author: Jarek Potiuk <[email protected]>
AuthorDate: Thu Apr 30 11:57:47 2026 +0200

    docs(secure-agent-setup): sandbox-bypass warning hook + private-repo sync 
pattern (#17)
    
    * 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)
    
    * docs(secure-agent-setup): regenerate doctoc TOC for new sections
    
    The previous commit (35bf73d) flagged that the doctoc-managed TOC
    at the top of `secure-agent-setup.md` was not updated when the two
    new sections — "Sandbox-bypass visibility hook" and "Syncing
    user-scope config across machines" — were added. This commit
    regenerates the TOC to include those sections plus their H3
    subsections (maxlevel=3, matching `.pre-commit-config.yaml`).
    
    doctoc itself was not run to produce this — it is not installed in
    the local environment and the sandbox blocks the npm/uv network
    access needed to fetch it. The anchors were instead derived by
    hand following the same encoding rules already visible in the
    existing TOC entries in this file:
    
    - spaces → `-`
    - in-word hyphens preserved (e.g. `user-scope`)
    - parentheses, asterisks, periods, backticks dropped without a separator
    - em-dash / slash with surrounding spaces → `--`
    
    A future doctoc run (e.g. via the pre-commit hook in CI) should
    produce a no-op diff against this TOC.
    
    Generated-by: Claude Code (Claude Opus 4.7)
    
    * docs(secure-agent-setup): sandbox-state status line for the terminal 
footer
    
    Adds a `statusLine` helper that makes the active sandbox state of
    the current Claude Code session visible at all times — green
    `[sandbox]` when `sandbox.enabled: true` is set in the resolved
    `settings.json`, bold-red `[NO SANDBOX]` otherwise. The indicator
    sits in the terminal footer that Claude Code already renders, so
    no extra UI surface is added; it just makes the existing footer
    informative about the *one* configuration property the rest of
    this document is about.
    
    Why it is needed even with the existing sandbox-bypass-warn hook:
    the `PreToolUse` hook fires per-call when the model requests a
    bypass, but a session whose `sandbox.enabled` was simply never set
    (e.g. an unrelated project's `.claude/settings.json` is missing,
    the user is in `~`, settings drift after a Claude Code upgrade)
    emits no signal until something goes wrong. The status line covers
    that gap by surfacing settings-level state continuously.
    
    Three changes:
    
    - `tools/agent-isolation/sandbox-status-line.sh` — the helper.
      Reads the statusLine JSON on stdin, resolves
      `.sandbox.enabled` from project- then user-scope `settings.json`
      via `jq`, prints `<model> [sandbox]` (green) or
      `<model> [NO SANDBOX]` (bold red). ASF license header matching
      the sibling scripts; executable bit set.
    - `secure-agent-setup.md` — new H2 "Sandbox-state status line"
      between the bypass-warn hook section and the sync-config section
      (sibling user-scope observability features). Adopter step 5 now
      recommends both, the sync-config layout, "what to track" table
      and fresh-host setup snippet pick up the new script. TOC entry
      added by hand following the same encoding rule as the previous
      TOC regen — anchor `#sandbox-state-status-line`.
    - `tools/agent-isolation/README.md` — Files table picks up the
      new script.
    
    Trade-offs are documented in-line: the indicator is settings-level
    truth, not session-level truth (CLI flags like
    `--bypass-permissions` and runtime mode changes that override the
    file are not visible), and pairing with the per-call bypass-warn
    hook is the recommended belt-and-braces posture. A future Claude
    Code statusLine schema that exposes sandbox state directly would
    let the helper drop the file-read.
    
    Generated-by: Claude Code (Claude Opus 4.7)
---
 secure-agent-setup.md                        | 347 +++++++++++++++++++++++++++
 tools/agent-isolation/README.md              |   2 +
 tools/agent-isolation/sandbox-bypass-warn.sh |  97 ++++++++
 tools/agent-isolation/sandbox-status-line.sh |  87 +++++++
 4 files changed, 533 insertions(+)

diff --git a/secure-agent-setup.md b/secure-agent-setup.md
index a90557d..0e3e6eb 100644
--- a/secure-agent-setup.md
+++ b/secure-agent-setup.md
@@ -12,6 +12,18 @@
     - [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)
   - [The clean-env wrapper](#the-clean-env-wrapper)
+  - [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)
+    - [Verify](#verify)
+    - [Trade-offs](#trade-offs)
+  - [Sandbox-state status line](#sandbox-state-status-line)
+  - [Syncing user-scope config across 
machines](#syncing-user-scope-config-across-machines)
+    - [What to track, what not to track](#what-to-track-what-not-to-track)
+    - [Layout](#layout)
+    - [Setting up a fresh host](#setting-up-a-fresh-host)
+    - [A minimal `sync.sh`](#a-minimal-syncsh)
+    - [Why a *private* repo](#why-a-private-repo)
   - [Adopter setup](#adopter-setup)
   - [Verification](#verification)
   - [Residual risks](#residual-risks)
@@ -383,6 +395,327 @@ 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.
+
+## Sandbox-state status line
+
+The Claude Code terminal footer (`statusLine`) is the
+always-visible bottom-of-window line that renders the model name,
+context usage, and any custom information you wire in. It is the
+right place to surface whether the sandbox is currently active for
+this session — a session that is inadvertently running with
+`sandbox.enabled` unset (or globally bypassed) cannot then drift
+unnoticed for hours.
+
+The framework ships
+[`tools/agent-isolation/sandbox-status-line.sh`](tools/agent-isolation/sandbox-status-line.sh)
+to render exactly that:
+
+- `<model> [sandbox]` in green when the active `settings.json`
+  sets `"sandbox": { "enabled": true }`, OR
+- `<model> [NO SANDBOX]` in bold red when it does not.
+
+Like the [Sandbox-bypass visibility hook](#sandbox-bypass-visibility-hook),
+this is **complementary**, not authoritative — see Trade-offs
+below.
+
+**Why user-scope.** Same reasoning as the bypass-warn hook: a
+session that runs without the sandbox is just as worth flagging
+in an unrelated project as in a tracker. Install in
+`~/.claude/settings.json` so the indicator shows in every session
+on the host, not only sessions inside a tracker repo whose
+project-level `.claude/settings.json` would otherwise have to wire
+it itself.
+
+**Install (user-scope).**
+
+```bash
+mkdir -p ~/.claude/scripts
+cp /path/to/airflow-steward/tools/agent-isolation/sandbox-status-line.sh \
+    ~/.claude/scripts/sandbox-status-line.sh
+chmod +x ~/.claude/scripts/sandbox-status-line.sh
+```
+
+Wire it into `~/.claude/settings.json` under the `statusLine` key:
+
+```jsonc
+{
+  "statusLine": {
+    "type": "command",
+    "command": "~/.claude/scripts/sandbox-status-line.sh"
+  }
+}
+```
+
+If you already maintain a richer custom statusLine, the helper is
+intentionally one-line — call it as one segment of your own
+renderer rather than replacing it.
+
+**Verify.**
+
+```bash
+echo '{"model":{"display_name":"Sonnet 
4.6"},"workspace":{"current_dir":"'"$PWD"'"}}' \
+    | ~/.claude/scripts/sandbox-status-line.sh
+```
+
+Expected output, *inside* this repo (its
+[`.claude/settings.json`](.claude/settings.json) sets
+`sandbox.enabled: true`): `Sonnet 4.6 [sandbox]` with `[sandbox]`
+rendered in green. From a directory whose `.claude/settings.json`
+does **not** enable the sandbox (or does not exist) and whose
+`~/.claude/settings.json` likewise does not set
+`sandbox.enabled: true`, the output is `[NO SANDBOX]` in bold red.
+
+**Trade-offs.**
+
+- **Settings-level truth, not session-level truth.** The script
+  reads `sandbox.enabled` from the file system. It cannot see CLI
+  flags (`--bypass-permissions`, equivalent runtime overrides) or
+  in-session permission-mode changes that override the file —
+  those still display as `[sandbox]` even though the running
+  session is unprotected. Pair the indicator with the
+  [Sandbox-bypass visibility hook](#sandbox-bypass-visibility-hook)
+  so per-call bypass attempts also surface in real time.
+- **Schema robustness.** The Claude Code statusLine input JSON
+  does not currently expose sandbox state — we read settings.json
+  ourselves. If a future Claude Code release adds a sandbox field
+  to the statusLine input, the script can be simplified to read
+  that field directly. Until then the file-read approach is the
+  only option, with the trade-off above.
+
+## 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`, `scripts/sandbox-status-line.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
+│   └── sandbox-status-line.sh          # symlinked → 
~/.claude/scripts/sandbox-status-line.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 + sandbox-state status line
+mkdir -p ~/.claude/scripts
+ln -sfn ~/.claude-config/scripts/sandbox-bypass-warn.sh \
+    ~/.claude/scripts/sandbox-bypass-warn.sh
+ln -sfn ~/.claude-config/scripts/sandbox-status-line.sh \
+    ~/.claude/scripts/sandbox-status-line.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 +745,20 @@ 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)
+   *and* the sandbox-state status line per
+   [Sandbox-state status line](#sandbox-state-status-line). Both
+   apply to every Claude Code session on the host (not only
+   tracker sessions), so they belong 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 scripts, the status-line 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..34b7f10 100644
--- a/tools/agent-isolation/README.md
+++ b/tools/agent-isolation/README.md
@@ -28,6 +28,8 @@ 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. |
+| [`sandbox-status-line.sh`](sandbox-status-line.sh) | Claude Code 
`statusLine` helper. Renders `<model> [sandbox]` (green) or `<model> [NO 
SANDBOX]` (bold red) based on `sandbox.enabled` in the active `settings.json`, 
so the terminal footer always shows whether the current session is sandboxed. 
Recommended user-scope. |
 
 ## 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
diff --git a/tools/agent-isolation/sandbox-status-line.sh 
b/tools/agent-isolation/sandbox-status-line.sh
new file mode 100755
index 0000000..e3bc514
--- /dev/null
+++ b/tools/agent-isolation/sandbox-status-line.sh
@@ -0,0 +1,87 @@
+#!/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-status-line.sh — Claude Code statusLine helper.
+#
+# Renders one line for the terminal footer that always shows whether
+# Claude Code's filesystem/network sandbox is active in the current
+# session. Designed to make a session that is *not* sandboxed
+# obvious at a glance, so it cannot drift unnoticed for hours.
+#
+# Output:
+# - "<model> [sandbox]"   — green, when the active settings.json
+#                           sets `"sandbox": { "enabled": true }`.
+# - "<model> [NO SANDBOX]" — bold red, otherwise.
+#
+# Lookup order for `sandbox.enabled` (project-scope wins):
+#   1. <workspace.current_dir>/.claude/settings.json
+#   2. ~/.claude/settings.json
+# Anything not found in either file is treated as "not enabled".
+#
+# Caveat — the script reads settings *files*, not the live runtime
+# state. CLI flags (`--bypass-permissions`, `--no-sandbox`) and
+# in-session permission-mode changes that override the file are not
+# visible here, and will still show as `[sandbox]`. Pair with the
+# sibling `sandbox-bypass-warn.sh` PreToolUse hook for a per-call
+# signal.
+#
+# Wiring (user-scope, applies to every session on the host):
+#
+#   {
+#     "statusLine": {
+#       "type": "command",
+#       "command": "~/.claude/scripts/sandbox-status-line.sh"
+#     }
+#   }
+#
+# See `secure-agent-setup.md` → "Sandbox-state status line" for
+# install steps and the trade-off discussion.
+
+set -u
+
+input=$(cat)
+
+model=$(printf '%s' "$input" | jq -r '.model.display_name // .model.id // ""' 
2>/dev/null || true)
+cwd=$(printf '%s' "$input"   | jq -r '.workspace.current_dir // ""'           
2>/dev/null || true)
+
+sandbox=""
+for f in "${cwd:+$cwd/.claude/settings.json}" "$HOME/.claude/settings.json"; do
+  [ -n "$f" ] && [ -f "$f" ] || continue
+  v=$(jq -r '.sandbox.enabled // empty' "$f" 2>/dev/null || true)
+  if [ -n "$v" ]; then
+    sandbox="$v"
+    break
+  fi
+done
+
+esc=$(printf '\033')
+green="${esc}[1;32m"
+red="${esc}[1;31m"
+reset="${esc}[0m"
+
+if [ "$sandbox" = "true" ]; then
+  tag="${green}[sandbox]${reset}"
+else
+  tag="${red}[NO SANDBOX]${reset}"
+fi
+
+if [ -n "$model" ]; then
+  printf '%s %s\n' "$model" "$tag"
+else
+  printf '%s\n' "$tag"
+fi

Reply via email to