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 f0b1cde  feat(agent-isolation): add sandbox-status-line-rich.sh — 
opt-in richer status line (#29)
f0b1cde is described below

commit f0b1cdea15b655f8be9bbdce135ad8b20b7b7759
Author: Jarek Potiuk <[email protected]>
AuthorDate: Fri May 1 21:47:56 2026 +0200

    feat(agent-isolation): add sandbox-status-line-rich.sh — opt-in richer 
status line (#29)
    
    Adds a sibling to the minimal `sandbox-status-line.sh`. Same
    sandbox-state detection (green `[sandbox]` / bold-red `[NO SANDBOX]`),
    plus opt-in segments useful when running multiple Claude Code
    sessions across worktrees and repos:
    
    - Folder name colour-coded by a stable hash of its basename (each
      repo / worktree keeps the same colour across sessions). Inside a
      Claude Code worktree (`<source>/.claude/worktrees/<name>`) renders
      `<source>/<worktree>` with each segment hash-coloured independently.
    - Git branch + dirty marker + ahead/behind, all local-only (no
      network). `-uno` skips untracked-file scan to keep render time
      bounded.
    - PR title from `gh pr view`, cached per repo+branch (5 min on
      success, 60 s on miss). Silent when `gh` is missing,
      unauthenticated, or the branch has no PR. Wrapped in a portable
      timeout (with a `perl -e 'alarm'` fallback for systems where
      neither `timeout` nor `gtimeout` is installed) so a hung gh call
      cannot stall every status-line render.
    - Yellow `[sandbox-auto]` distinguishes
      `sandbox.autoAllowBashIfSandboxed: true` from plain `[sandbox]` —
      that flag widens the in-sandbox bash blast radius and is worth
      surfacing distinctly.
    
    Includes a portable `stat` chain (GNU `-c %Y` first, BSD `-f %m`
    fallback). The obvious "BSD first" ordering breaks on Linux: GNU
    `stat -f` is interpreted as "filesystem mode" and emits multi-line
    stdout that pollutes the next arithmetic line under `set -u`.
    
    The minimal `sandbox-status-line.sh` remains the documented default
    per `secure-agent-setup.md`. The rich variant is opt-in: copy the
    `-rich` file in place of the minimal one and point
    `statusLine.command` at it.
    
    `secure-agent-setup.md` and `tools/agent-isolation/README.md` get
    short pointers to the new file.
    
    Generated-by: Claude Code (Opus 4.7)
---
 secure-agent-setup.md                             |  12 +
 tools/agent-isolation/README.md                   |   1 +
 tools/agent-isolation/sandbox-status-line-rich.sh | 272 ++++++++++++++++++++++
 3 files changed, 285 insertions(+)

diff --git a/secure-agent-setup.md b/secure-agent-setup.md
index 06fde48..b283903 100644
--- a/secure-agent-setup.md
+++ b/secure-agent-setup.md
@@ -644,6 +644,18 @@ 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.
 
+For adopters who want a richer variant out of the box, the framework
+also ships
+[`tools/agent-isolation/sandbox-status-line-rich.sh`](tools/agent-isolation/sandbox-status-line-rich.sh).
+Same sandbox-state detection, plus folder name (hash-coloured for a
+stable per-repo identity), git branch + dirty marker + ahead/behind,
+per-branch PR title (cached for 5 min, silent when `gh` is missing or
+unauthenticated), and a yellow `[sandbox-auto]` tag for the
+`autoAllowBashIfSandboxed` setting. Install steps are identical —
+copy the `-rich` file in place of the minimal one and point
+`statusLine.command` at it. The minimal variant remains the
+documented default; the rich one is opt-in.
+
 **Verify.**
 
 ```bash
diff --git a/tools/agent-isolation/README.md b/tools/agent-isolation/README.md
index a5ceb50..f5cad04 100644
--- a/tools/agent-isolation/README.md
+++ b/tools/agent-isolation/README.md
@@ -30,6 +30,7 @@ versions.
 | [`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 — 
project `settings.local.json` first, then project `settings.json`, then 
user-scope, mirroring Claude Code's own precedence. Reflects in-session 
`/sandbox` toggles (which persist to project `settings.local.json`). 
Recommended user-scope. |
+| [`sandbox-status-line-rich.sh`](sandbox-status-line-rich.sh) | Opt-in richer 
alternative to `sandbox-status-line.sh`. Same sandbox-state detection, plus 
folder name (hash-coloured), git branch + dirty + ahead/behind, per-branch PR 
title (cached, gated by `gh`), and a yellow `[sandbox-auto]` tag for the 
`autoAllowBashIfSandboxed` setting. Wire one *or* the other into 
`statusLine.command`. |
 
 ## Usage at a glance
 
diff --git a/tools/agent-isolation/sandbox-status-line-rich.sh 
b/tools/agent-isolation/sandbox-status-line-rich.sh
new file mode 100755
index 0000000..4f007c3
--- /dev/null
+++ b/tools/agent-isolation/sandbox-status-line-rich.sh
@@ -0,0 +1,272 @@
+#!/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-rich.sh — Claude Code statusLine helper, rich variant.
+#
+# Drop-in alternative to the minimal sibling
+# `sandbox-status-line.sh`. Same sandbox-state detection (green
+# `[sandbox]` / yellow `[sandbox-auto]` / bold-red `[NO SANDBOX]`),
+# plus extra context segments useful when running several Claude
+# Code sessions across worktrees and repos:
+#
+#   [sandbox]  <folder>   | <branch><dirty><ahead/behind>
+#                         | #<PR> <title>
+#                         | <model>
+#
+# - Folder name is colour-coded by a stable hash of its basename, so
+#   each repo / worktree keeps the same colour across sessions.
+# - Inside a Claude Code worktree (`<source>/.claude/worktrees/<name>`)
+#   renders `<source>/<worktree>` with each segment hash-coloured
+#   independently — the source colour stays stable across worktrees.
+# - Git segment is local-only (no network): branch, dirty marker
+#   (`-uno` skips untracked-file scan), ahead/behind vs upstream from
+#   cached refs.
+# - PR segment runs `gh pr view` once per branch with a 5-min success
+#   / 60-s miss cache under `${XDG_CACHE_HOME:-~/.cache}/claude-statusline/`.
+#   Silent when `gh` is missing, unauthenticated, or the branch has no
+#   PR. Wrapped in a portable `timeout` (with a `perl -e 'alarm'`
+#   fallback for systems where neither `timeout` nor `gtimeout` is
+#   installed) so a hung gh call cannot stall every status-line render.
+# - Yellow `[sandbox-auto]` lights up when the active settings set
+#   `.sandbox.autoAllowBashIfSandboxed: true` — that flag widens blast
+#   radius (bash inside the sandbox skips the per-call allow prompt)
+#   and is worth surfacing distinctly from plain `[sandbox]`.
+#
+# Sandbox tag reads `.sandbox.enabled` from the active settings,
+# walked in Claude Code's standard precedence order (most-specific
+# first):
+#   1. <cwd>/.claude/settings.local.json
+#   2. <cwd>/.claude/settings.json
+#   3. ~/.claude/settings.local.json
+#   4. ~/.claude/settings.json
+# First file with `.sandbox.enabled` set (true *or* false) wins. The
+# `/sandbox` slash-command toggle persists to project-scope
+# `settings.local.json`, so reading that file is what makes the prefix
+# update after an in-session toggle. CLI flags like
+# `--bypass-permissions` are still not visible here — pair with
+# `sandbox-bypass-warn.sh` for per-call signal.
+#
+# Wiring (user-scope):
+#
+#   {
+#     "statusLine": {
+#       "type": "command",
+#       "command": "~/.claude/scripts/sandbox-status-line-rich.sh"
+#     }
+#   }
+#
+# See `secure-agent-setup.md` → "Sandbox-state status line" for the
+# install steps and the trade-off discussion. The minimal sibling
+# remains the documented default; this variant is opt-in.
+
+set -u
+
+input=$(cat)
+
+# ---------------------------------------------------------------------------
+# 0. Parse stdin once — pull out cwd and model display name.
+# ---------------------------------------------------------------------------
+parsed=$(printf '%s' "$input" | python3 -c 'import json,sys
+try:
+    d = json.load(sys.stdin)
+except Exception:
+    print(); sys.exit(0)
+cwd = d.get("cwd") or (d.get("workspace") or {}).get("current_dir") or ""
+m = d.get("model") or {}
+model = m.get("display_name") or m.get("id") or ""
+print(f"{cwd}\t{model}")' 2>/dev/null)
+cwd=${parsed%%$'\t'*}
+model=${parsed#*$'\t'}
+[ -z "$cwd" ] && cwd="$PWD"
+
+# ---------------------------------------------------------------------------
+# 1. Folder (colour-coded by hash)
+#    Inside a Claude worktree (<source>/.claude/worktrees/<name>) we render
+#    "<source>/<worktree>" with each segment hash-coloured independently —
+#    the source colour stays stable across all worktrees of the same repo.
+# ---------------------------------------------------------------------------
+folder=$(basename "$cwd")
+worktree_name=""
+if [[ "$cwd" == *"/.claude/worktrees/"* ]]; then
+    worktree_name="$folder"
+    folder=$(basename "${cwd%%/.claude/worktrees/*}")
+fi
+
+palette=(33 39 45 51 75 81 87 105 111 117 123 141 147 153 159 165 171 177 183 
189 195 201 207 213 219 220 226 214 208 202 196 199 203 209 215 221 227 154 118 
82 46 47 48 49 50 84 120 156 190 228)
+n=${#palette[@]}
+idx=$(printf '%s' "$folder" | cksum | awk '{print $1}')
+color=${palette[$((idx % n))]}
+folder_out=$(printf '\033[38;5;%dm%s\033[0m' "$color" "$folder")
+
+if [ -n "$worktree_name" ]; then
+    wt_idx=$(printf '%s' "$worktree_name" | cksum | awk '{print $1}')
+    wt_color=${palette[$((wt_idx % n))]}
+    wt_out=$(printf '\033[38;5;%dm%s\033[0m' "$wt_color" "$worktree_name")
+    folder_out="$folder_out/$wt_out"
+fi
+
+# ---------------------------------------------------------------------------
+# 2. Git status
+# ---------------------------------------------------------------------------
+git_info=""
+branch=""
+
+if git_dir=$(git -C "$cwd" rev-parse --git-dir 2>/dev/null); then
+    if [[ "$git_dir" != /* ]]; then
+        git_dir="$cwd/$git_dir"
+    fi
+    head_file="$git_dir/HEAD"
+    if [ -f "$head_file" ]; then
+        head_content=$(cat "$head_file")
+        if [[ "$head_content" == ref:* ]]; then
+            branch="${head_content#ref: refs/heads/}"
+        else
+            branch="${head_content:0:7} (detached)"
+        fi
+    fi
+
+    dirty=""
+    if [ -n "$(git -C "$cwd" status --porcelain --no-renames -uno 
2>/dev/null)" ]; then
+        dirty="*"
+    fi
+
+    ahead_behind=""
+    if upstream=$(git -C "$cwd" rev-parse --abbrev-ref "@{u}" 2>/dev/null); 
then
+        if counts=$(git -C "$cwd" rev-list --left-right --count "HEAD...@{u}" 
2>/dev/null); then
+            ahead=$(printf '%s' "$counts" | awk '{print $1}')
+            behind=$(printf '%s' "$counts" | awk '{print $2}')
+            [ "${ahead:-0}" -gt 0 ] 2>/dev/null && ahead_behind="+$ahead"
+            [ "${behind:-0}" -gt 0 ] 2>/dev/null && 
ahead_behind="${ahead_behind}-$behind"
+            [ -n "$ahead_behind" ] && ahead_behind=" $ahead_behind"
+        fi
+    fi
+
+    [ -n "$branch" ] && git_info=" | $branch$dirty$ahead_behind"
+fi
+
+# ---------------------------------------------------------------------------
+# 3. PR title (cached per repo+branch)
+# ---------------------------------------------------------------------------
+pr_info=""
+if [ -n "$branch" ] && [[ "$branch" != *"(detached)" ]] && command -v gh 
>/dev/null 2>&1; then
+    cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/claude-statusline"
+    mkdir -p "$cache_dir" 2>/dev/null
+    if repo_root=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null); then
+        safe_key=$(printf '%s/%s' "$repo_root" "$branch" | tr '/ ' '__')
+        cache_file="$cache_dir/pr_${safe_key}"
+
+        # TTL: 5 min on success, 60 s on miss so we re-probe quickly after
+        # `gh auth login`, a PR open, or gh being installed.
+        now=$(date +%s)
+        cache_mtime=0
+        # GNU stat (Linux) first, BSD stat (macOS) fallback. Order
+        # matters: on Linux, BSD's `-f %m` is misread as "filesystem
+        # mode" and stat exits 0 with multi-line garbage on stdout
+        # that breaks the arithmetic on the next line under `set -u`.
+        [ -f "$cache_file" ] && cache_mtime=$(stat -c %Y "$cache_file" 
2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0)
+        age=$(( now - cache_mtime ))
+        ttl=300
+        [ -s "$cache_file" ] || ttl=60
+
+        if [ "$age" -ge "$ttl" ]; then
+            # Portable timeout wrapper: prefer `timeout` / `gtimeout`; fall
+            # back to perl (present on macOS by default; plain `timeout` is
+            # not, which silently broke this segment previously).
+            if command -v timeout >/dev/null 2>&1; then
+                tmo=(timeout 3)
+            elif command -v gtimeout >/dev/null 2>&1; then
+                tmo=(gtimeout 3)
+            else
+                tmo=(perl -e 'alarm shift; exec @ARGV' 3)
+            fi
+            pr_raw=$(cd "$cwd" && "${tmo[@]}" gh pr view --json number,title 
--jq '"#\(.number) \(.title)"' 2>/dev/null || true)
+            printf '%s' "$pr_raw" > "$cache_file"
+        else
+            pr_raw=$(cat "$cache_file")
+        fi
+
+        if [ -n "$pr_raw" ]; then
+            if [ "${#pr_raw}" -gt 55 ]; then
+                pr_raw="${pr_raw:0:55}..."
+            fi
+            pr_info=" | $pr_raw"
+        fi
+    fi
+fi
+
+# ---------------------------------------------------------------------------
+# 4. Sandbox state — green [sandbox] / yellow [sandbox-auto] / bold-red
+#    [NO SANDBOX] suffix. Walks settings.local.json + settings.json in
+#    project then user scope. First file with `.sandbox.enabled` set
+#    (true *or* false) wins — `/sandbox` writes to project
+#    `settings.local.json`, so that is the file that flips when the user
+#    toggles in-session. The yellow `[sandbox-auto]` variant lights up
+#    when `.sandbox.autoAllowBashIfSandboxed` is true in the same file —
+#    it signals that bash commands inside the sandbox skip the per-call
+#    allow prompt, which is a wider blast radius than vanilla sandbox.
+# ---------------------------------------------------------------------------
+sandbox=""
+sandbox_auto=""
+for f in \
+    "${cwd:+$cwd/.claude/settings.local.json}" \
+    "${cwd:+$cwd/.claude/settings.json}" \
+    "$HOME/.claude/settings.local.json" \
+    "$HOME/.claude/settings.json"; do
+    [ -n "$f" ] && [ -f "$f" ] || continue
+    v=$(python3 -c 'import json,sys
+try:
+    s = json.load(open(sys.argv[1])).get("sandbox",{})
+    e = s.get("enabled")
+    a = s.get("autoAllowBashIfSandboxed")
+    en = "true" if e is True else ("false" if e is False else "")
+    au = "true" if a is True else ("false" if a is False else "")
+    print(f"{en}\t{au}")
+except Exception:
+    pass' "$f" 2>/dev/null)
+    en=${v%%$'\t'*}
+    au=${v#*$'\t'}
+    if [ -n "$en" ]; then
+        sandbox="$en"
+        sandbox_auto="$au"
+        break
+    fi
+done
+
+esc=$(printf '\033')
+green="${esc}[1;32m"
+yellow="${esc}[1;33m"
+red="${esc}[1;31m"
+reset="${esc}[0m"
+
+if [ "$sandbox" = "true" ]; then
+    if [ "$sandbox_auto" = "true" ]; then
+        sandbox_tag="${yellow}[sandbox-auto]${reset}"
+    else
+        sandbox_tag="${green}[sandbox]${reset}"
+    fi
+else
+    sandbox_tag="${red}[NO SANDBOX]${reset}"
+fi
+
+model_info=""
+[ -n "$model" ] && model_info=" | $model"
+
+# ---------------------------------------------------------------------------
+# 5. Emit — sandbox tag leads, then folder / git / PR / model.
+# ---------------------------------------------------------------------------
+printf '%s %s%s%s%s' "$sandbox_tag" "$folder_out" "$git_info" "$pr_info" 
"$model_info"

Reply via email to