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 721c639  feat(agent-isolation): add opt-in terminal-tint helper for 
waiting state (#456)
721c639 is described below

commit 721c639e31041e3ffe1c8acf27d6ee11c4dffe89
Author: Jarek Potiuk <[email protected]>
AuthorDate: Fri Jun 5 18:35:13 2026 +0200

    feat(agent-isolation): add opt-in terminal-tint helper for waiting state 
(#456)
    
    Ship claude-term-bg.sh, an optional quality-of-life helper (not a
    security control) that keeps a calm baseline terminal background and
    tints it only when Claude Code genuinely wants the operator to act —
    never while it is working.
    
    Two states across five user-scope hooks: Stop -> wait (turn finished,
    tint); Notification -> notify (tint only for permission prompts, calm
    for the plain idle ping); and PreToolUse + UserPromptSubmit +
    SessionStart -> reset (calm while working, after a reply, and on a
    fresh session — SessionStart also clears a stale tint a prior session
    left in the terminal).
    
    Two mechanics the helper gets right and the doc explains:
    
    - Hooks run with no controlling terminal, so /dev/tty is useless;
      find_tty_dev() walks the process tree to the Claude pty and writes
      the OSC escape there.
    - iTerm2 honours OSC 11 (set) but not OSC 111 (reset), so the only
      deterministic reset is an explicit colour via CLAUDE_RESET_BG; the
      fallback emits OSC 111 plus iTerm2's SetColors=bg=default.
    
    Wires into the secure-agent-setup doc as an explicitly-optional,
    default-off install step (asked during setup, mirroring the status
    line), with the helper added to the agent-isolation README and a new
    "Waiting-for-input terminal tint" section (Install / Verify /
    Trade-offs). Framed throughout as a UX convenience, not a security
    mechanism.
    
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 docs/setup/secure-agent-setup.md        | 154 +++++++++++++++++++++++++++++++-
 tools/agent-isolation/README.md         |   1 +
 tools/agent-isolation/claude-term-bg.sh | 100 +++++++++++++++++++++
 3 files changed, 254 insertions(+), 1 deletion(-)

diff --git a/docs/setup/secure-agent-setup.md b/docs/setup/secure-agent-setup.md
index e352144..1e5ff5d 100644
--- a/docs/setup/secure-agent-setup.md
+++ b/docs/setup/secure-agent-setup.md
@@ -32,6 +32,7 @@
     - [Verify](#verify-1)
     - [Trade-offs](#trade-offs-1)
   - [Sandbox-state status line](#sandbox-state-status-line)
+  - [Waiting-for-input terminal tint](#waiting-for-input-terminal-tint)
   - [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)
@@ -1158,6 +1159,141 @@ bold red.
   read that field directly. Until then the file-read approach is
   the only option, with the trade-off above.
 
+## Waiting-for-input terminal tint
+
+> **Quality-of-life helper, not a security control.** Unlike the
+> rest of this document, this piece protects nothing — it just
+> makes the "Claude is blocked on me" state impossible to miss. It
+> rides the same user-scope-hook install machinery as the helpers
+> above, which is why it lives here, but it is entirely optional
+> and off by default.
+
+When you run several agents across tabs, it is easy to leave one
+sitting at a permission prompt or a finished turn while you work
+elsewhere. The framework ships
+[`tools/agent-isolation/claude-term-bg.sh`](../../tools/agent-isolation/claude-term-bg.sh)
+to make a **calm baseline the normal state and tint the background
+only when Claude genuinely wants you to act** — never while it is
+working. The model is two states, wired across five hooks:
+
+| Moment | Hook → action | Background |
+|---|---|---|
+| Turn finished — your turn to respond | `Stop` → `wait` | tinted (muted 
indigo `#2a1a3a`) |
+| Blocked on a permission prompt | `Notification` → `notify` | tinted |
+| Actively working (running a tool) | `PreToolUse` → `reset` | calm |
+| Fresh/idle session, plain idle ping | `SessionStart` / `Notification` → 
`reset`/`notify` | calm |
+| You submit a reply | `UserPromptSubmit` → `reset` | calm |
+
+Three details make the model behave:
+
+- **`PreToolUse` → `reset`** fires on every tool call, so the moment
+  Claude resumes work — including right after you approve a
+  permission prompt that had tinted the screen — it returns to calm.
+  Without it, an approved-permission tint would linger until the
+  turn ended.
+- **`SessionStart` → `reset`** clears any tint a *previous* session
+  left behind (OSC background changes persist in the terminal
+  across processes, so a session closed mid-wait would otherwise
+  hand its tint to the next one — making a "fresh" session look
+  like it is waiting on you).
+- **`Notification` → `notify`** is selective: the same hook fires
+  both for permission prompts *and* the plain 60-second idle ping,
+  so the script reads the notification payload on stdin and tints
+  only when the message is a permission/attention prompt; an idle
+  session stays calm.
+
+**Two mechanics make this work** (both are easy to get wrong):
+
+1. **Hooks have no controlling terminal.** Claude Code spawns hook
+   commands detached from the tty, so `/dev/tty` does not resolve
+   to your window — a naive `printf '\033]11;…' > /dev/tty` writes
+   nowhere. The script walks up the process tree from `$PPID` to
+   find the Claude process's pty (e.g. `/dev/ttys003`) and writes
+   the escape straight to that device.
+2. **Set and reset are not symmetric.** iTerm2 honours OSC 11 (set
+   background) but does **not** reliably honour OSC 111
+   (reset-to-default) through Claude's fullscreen TUI, so a naive
+   reset leaves the tint stuck on. The script resets
+   belt-and-braces: it emits both OSC 111 *and* iTerm2's
+   proprietary `SetColors=bg=default`. For a guaranteed reset on
+   any terminal, set `CLAUDE_RESET_BG` to your normal background
+   colour and the script re-applies it via OSC 11 (the path that
+   is known to work since the tint itself does).
+
+**Why user-scope.** Same reasoning as the helpers above: you want
+the signal in every session on the host, not only tracker
+sessions. Install in `~/.claude/settings.json`.
+
+**Install (user-scope).**
+
+```bash
+mkdir -p ~/.claude/scripts
+cp /path/to/airflow-steward/tools/agent-isolation/claude-term-bg.sh \
+    ~/.claude/scripts/claude-term-bg.sh
+chmod +x ~/.claude/scripts/claude-term-bg.sh
+```
+
+Wire it into `~/.claude/settings.json` under four hook events. If
+you already have hooks on any of these events, add the command as
+an extra entry rather than replacing the existing array. The
+`CLAUDE_RESET_BG=#000000` prefix makes the calm state a
+deterministic black (recommended — it sidesteps the OSC-111 reset
+gap described above); drop it to fall back to profile-default
+reset.
+
+```jsonc
+{
+  "hooks": {
+    "Stop": [
+      { "hooks": [ { "type": "command", "command": 
"~/.claude/scripts/claude-term-bg.sh wait" } ] }
+    ],
+    "PreToolUse": [
+      { "matcher": "*", "hooks": [ { "type": "command", "command": 
"CLAUDE_RESET_BG=#000000 ~/.claude/scripts/claude-term-bg.sh reset" } ] }
+    ],
+    "UserPromptSubmit": [
+      { "hooks": [ { "type": "command", "command": "CLAUDE_RESET_BG=#000000 
~/.claude/scripts/claude-term-bg.sh reset" } ] }
+    ],
+    "SessionStart": [
+      { "hooks": [ { "type": "command", "command": "CLAUDE_RESET_BG=#000000 
~/.claude/scripts/claude-term-bg.sh reset" } ] }
+    ],
+    "Notification": [
+      { "hooks": [ { "type": "command", "command": "CLAUDE_RESET_BG=#000000 
~/.claude/scripts/claude-term-bg.sh notify" } ] }
+    ]
+  }
+}
+```
+
+Override the colours via the environment: `CLAUDE_WAIT_BG`
+(default `#2a1a3a`) and `CLAUDE_RESET_BG` (default unset → reset to
+the profile default; set to a colour like `#000000` for a
+deterministic calm background).
+
+**Verify.** The hooks fire on real events, so the quickest check
+is live: start a session, let a turn finish, and confirm the
+background tints; then send a reply and confirm it resets. To
+sanity-check the script in isolation, run it against your own
+terminal device:
+
+```bash
+~/.claude/scripts/claude-term-bg.sh wait   # background tints
+~/.claude/scripts/claude-term-bg.sh reset  # background restored
+```
+
+(Run directly from an interactive shell, the script finds the
+shell's pty via `$PPID` and writes there.)
+
+**Trade-offs.**
+
+- **iTerm2-tested, fail-soft elsewhere.** The set path uses OSC 11,
+  which most modern terminal emulators support; terminals that
+  ignore it simply show no change. The reset path is hardened for
+  iTerm2's OSC-111 gap specifically. On a terminal where reset
+  misbehaves, set `CLAUDE_RESET_BG` for a deterministic restore.
+- **Cosmetic only.** It conveys no sandbox or permission state —
+  pair it with the [Sandbox-state status line](#sandbox-state-status-line)
+  and [Sandbox-bypass visibility hook](#sandbox-bypass-visibility-hook)
+  for the security-relevant signals.
+
 ## Syncing user-scope config across machines
 
 The user-scope pieces of the secure setup —
@@ -1545,7 +1681,23 @@ Then walk through:
    (e.g. I have other PreToolUse hooks for unrelated work),
    surface the merge diff and ask me to approve before writing.
 
-6. **Verify.** After everything is in place, walk through the
+6. **(Optional) Waiting-for-input terminal tint.** Ask me whether
+   I want the terminal background to tint while Claude is waiting
+   on me (a pure quality-of-life signal, no security effect).
+   **Default no.** Only if I say yes: copy
+   `<airflow-steward>/tools/agent-isolation/claude-term-bg.sh`
+   into `~/.claude/scripts/` and `chmod +x` it, then add five
+   hooks to `~/.claude/settings.json`, merging into any existing
+   arrays on those events — `Stop` → `claude-term-bg.sh wait`;
+   `UserPromptSubmit`, `SessionStart`, and `PreToolUse` (matcher
+   `*`) → `claude-term-bg.sh reset`; and `Notification` →
+   `claude-term-bg.sh notify`. Ask whether I want the calm state
+   to be a deterministic black (prefix the reset/notify commands
+   with `CLAUDE_RESET_BG=#000000`) or the terminal's profile
+   default. See
+   [Waiting-for-input terminal tint](#waiting-for-input-terminal-tint).
+
+7. **Verify.** After everything is in place, walk through the
    Verification checks from the next section of this document
    ("Verification — Via a Claude Code prompt") and report
    ✓ done / ✗ missing / ⚠ partial for each piece.
diff --git a/tools/agent-isolation/README.md b/tools/agent-isolation/README.md
index 2e07f89..228f915 100644
--- a/tools/agent-isolation/README.md
+++ b/tools/agent-isolation/README.md
@@ -34,6 +34,7 @@ versions.
 | [`sandbox-error-hint.sh`](sandbox-error-hint.sh) | Claude Code `PostToolUse` 
hook (Bash matcher). Scans the tool's stdout + stderr for the three known 
sandbox-shaped error signatures (SSH agent / Yubikey unreachable, loopback 
port-bind blocked, docker / podman socket denied) and prints a `[sandbox-hint]` 
line pointing at the matching entry in 
[`docs/setup/sandbox-troubleshooting.md`](../../docs/setup/sandbox-troubleshooting.md).
 Fail-open: any unexpected JSON shape exits silent. Recomm [...]
 | [`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`. |
+| [`claude-term-bg.sh`](claude-term-bg.sh) | **Opt-in quality-of-life helper 
(not a security control).** Keeps a calm baseline background and tints it only 
when Claude genuinely wants you to act (never while working), so a window 
you've tabbed away from can't sit blocked unnoticed. Two states across five 
hooks: `Stop` → `wait` (turn finished, tint); `UserPromptSubmit` + 
`SessionStart` + `PreToolUse` → `reset` (calm — `PreToolUse` keeps it calm 
while a tool runs and clears an approved-per [...]
 | [`sandbox-add-project-root.sh`](sandbox-add-project-root.sh) | Adds the 
current adopter repo's project root (and, with `--all-worktrees`, every linked 
git worktree's working dir) as an explicit absolute path to 
`sandbox.filesystem.allowRead` and `allowWrite` in the project-local, 
gitignored `<repo>/.claude/settings.local.json` — one entry per worktree, each 
in that worktree's own settings file. Defensive against [issue 
#197](https://github.com/apache/airflow-steward/issues/197) — `allo [...]
 | [`git-global-post-checkout.sh`](git-global-post-checkout.sh) | Universal 
`post-checkout` git hook installed at `~/.claude/git-hooks/post-checkout` when 
the operator picks **whole-user** scope in `setup-isolated-setup-install`. 
Activated by `git config --global core.hooksPath ~/.claude/git-hooks/` so every 
`git checkout` / `git clone` / `git worktree add` across the host invokes it. 
Two responsibilities (both best-effort + idempotent + `\|\| true`): (1) `setup 
verify --auto-fix-symlinks [...]
 
diff --git a/tools/agent-isolation/claude-term-bg.sh 
b/tools/agent-isolation/claude-term-bg.sh
new file mode 100755
index 0000000..d8f730f
--- /dev/null
+++ b/tools/agent-isolation/claude-term-bg.sh
@@ -0,0 +1,100 @@
+#!/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.
+#
+# claude-term-bg.sh — tint the terminal background while Claude Code is
+# waiting on YOU, and keep it calm the rest of the time.
+#
+# This is a quality-of-life helper, NOT a security control: it makes the
+# "Claude is blocked on me" state impossible to miss in a window you've
+# tabbed away from. Wire it into five Claude Code hooks (user-scope):
+#
+#   "Stop"              -> claude-term-bg.sh wait    (turn finished — your 
turn, tint)
+#   "UserPromptSubmit"  -> claude-term-bg.sh reset   (you replied — back to 
calm)
+#   "SessionStart"      -> claude-term-bg.sh reset   (fresh session — clear 
any stale tint)
+#   "PreToolUse"        -> claude-term-bg.sh reset   (actively working — stay 
calm; also
+#                                                     clears an 
approved-permission tint)
+#   "Notification"      -> claude-term-bg.sh notify  (tint for permission 
prompts only;
+#                                                     the plain idle ping 
stays calm)
+#
+# Colours are overridable via the environment (e.g. inline in the hook 
command):
+#   CLAUDE_WAIT_BG    background while waiting   (default: #2a1a3a, a muted 
indigo)
+#   CLAUDE_RESET_BG   calm/idle background       (default: unset -> reset to 
profile default;
+#                                                 set e.g. to #000000 for a 
deterministic black)
+#
+# Mechanism notes (the two things that make this actually work):
+#
+#   1. No controlling terminal. Claude Code spawns hook commands detached
+#      from the tty, so /dev/tty does not resolve to your terminal window.
+#      find_tty_dev() walks up the process tree to the Claude process's pty
+#      (e.g. /dev/ttys003) and writes the escape straight to that device.
+#
+#   2. Set vs. reset asymmetry. iTerm2 honours OSC 11 (set background) but
+#      does NOT reliably honour OSC 111 (reset-to-default) through Claude's
+#      fullscreen TUI, so a naive reset leaves the tint stuck. The only
+#      deterministic reset is an explicit colour via CLAUDE_RESET_BG (OSC 11,
+#      the path we know works); without it we emit BOTH OSC 111 and iTerm2's
+#      proprietary SetColors=bg=default and let whichever the terminal
+#      understands win.
+#
+# Tested on iTerm2 + macOS. Terminals that ignore OSC 11 entirely simply see
+# no change (fail-soft). For a guaranteed reset anywhere, set CLAUDE_RESET_BG.
+set -u
+WAIT_BG="${CLAUDE_WAIT_BG:-#2a1a3a}"
+RESET_BG="${CLAUDE_RESET_BG:-}"
+action="${1:-reset}"
+
+find_tty_dev() {
+  local pid=${PPID:-$$} t i
+  for i in 1 2 3 4 5 6 7 8; do
+    [ -z "$pid" ] || [ "$pid" = "0" ] || [ "$pid" = "1" ] && break
+    t=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ')
+    case "$t" in
+      ttys*|tty[0-9]*) echo "/dev/$t"; return 0 ;;
+    esac
+    pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
+  done
+  return 1
+}
+
+tty_dev=$(find_tty_dev || true)
+[ -n "${tty_dev:-}" ] && [ -w "$tty_dev" ] || exit 0
+
+set_wait() { printf '\033]11;%s\007' "$WAIT_BG" > "$tty_dev"; }
+set_reset() {
+  if [ -n "$RESET_BG" ]; then
+    printf '\033]11;%s\007' "$RESET_BG" > "$tty_dev"
+  else
+    printf '\033]111\007' > "$tty_dev"                       # xterm: reset bg
+    printf '\033]1337;SetColors=bg=default\007' > "$tty_dev" # iTerm2 
proprietary
+  fi
+}
+
+case "$action" in
+  wait|set)  set_wait ;;
+  reset|off) set_reset ;;
+  notify)
+    # Notification fires for permission prompts AND the plain 60s idle ping.
+    # Tint only when it's something that genuinely wants the user to act.
+    msg=$(cat 2>/dev/null)
+    case "$msg" in
+      *permission*|*approve*|*"needs your"*) set_wait ;;
+      *)                                     set_reset ;;
+    esac
+    ;;
+esac
+exit 0

Reply via email to