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 339d3eb feat(hooks): sandbox-error-hint.sh — annotate sandbox-shaped
error output with catalog references (#293)
339d3eb is described below
commit 339d3eb904e1a359c4b30d3e819b4d31e03122b1
Author: Jarek Potiuk <[email protected]>
AuthorDate: Mon May 25 22:45:12 2026 +0200
feat(hooks): sandbox-error-hint.sh — annotate sandbox-shaped error output
with catalog references (#293)
PostToolUse hook that runs after every Bash tool call, scans
stdout + stderr for the three known sandbox-shaped error
signatures (catalogued in #291's
docs/setup/sandbox-troubleshooting.md), and prints a
`[sandbox-hint] ...` line pointing at the matching catalog entry.
Closes the discoverability loop on the troubleshooting catalog:
the user no longer has to remember the catalog exists when a
mid-flow failure looks sandbox-shaped -- the catalog reference
appears next to the error automatically.
Three failure signatures recognised today, matched against the
literal error strings the sandbox produces:
- SSH agent / Yubikey unreachable: `Could not open a connection
to your authentication agent` / `agent refused operation` /
`ssh-add: error fetching identities` / `Permission denied
(publickey)`.
- Docker / Podman socket denied: `Cannot connect to the Docker
daemon` / `open /var/run/docker.sock: operation not permitted`
/ `Cannot connect to Podman` / podman `connect: permission
denied`.
- Localhost port-bind blocked: `127.0.0.1 ... [Pp]ermission
denied` / `[Oo]peration not permitted ... bind` / `Errno 49 ...
assign requested address` / `Connection refused ... 127.0.0.1`.
Each match prints a one-line `[sandbox-hint]` to stderr with the
matching catalog anchor, plus a follow-up nudge to run
`/setup-isolated-setup-doctor` (#292) for a structured probe of
all three. Hook exits 1 so stderr surfaces as a tool-result
annotation; the tool's actual outcome is unchanged.
Hook is fail-open by design:
- Non-Bash tool calls exit 0 silently (the signatures are
Bash-stderr-shaped).
- Missing `tool_response`, missing `jq`, malformed JSON, any
unexpected envelope shape -> exit 0 silently.
- No signature match -> exit 0 silently.
A broken hint must never break a legitimate tool call.
Smoke-tested locally against canned JSON envelopes:
- Three signature matches -> hint fires, rc=1, anchor links
correct.
- No-match Bash output -> silent rc=0.
- Non-Bash tool name -> silent rc=0.
- Malformed JSON -> silent rc=0.
Files:
- NEW tools/agent-isolation/sandbox-error-hint.sh -- the script
(~120 lines). Follows the same shape and conventions as
sandbox-bypass-warn.sh.
- tools/agent-isolation/README.md -- new table row describing the
hook + its relationship with the doctor skill (#292).
- docs/setup/secure-agent-setup.md:
* New "Sandbox-error hint hook" section between the existing
"Sandbox-bypass visibility hook" and "Sandbox-state status
line" sections. Same install / verify / trade-offs shape as
the bypass-warn section. Includes the signature -> catalog
anchor table and the "complements the doctor skill"
relationship.
* Quick-start step 4 updated to mention the new hook alongside
bypass-warn and status-line.
* Sync-repo table updated to include the new script.
- docs/setup/sandbox-troubleshooting.md -- short intro section
explaining the two discoverability surfaces (doctor skill +
hint hook) and the lock-step rule: catalog grows -> doctor
grows + hint hook grows in the same change.
- .claude/skills/setup-isolated-setup-verify/SKILL.md -- checks 2
+ 3 updated to include the new hook script + its
`PostToolUse` wiring. Reports ⚠ (not ✗) on absence: the hook
is a discoverability aid, not a security guard; missing it
just means hints are not surfaced.
The install skill (setup-isolated-setup-install) needs no edit --
it defers the install walkthrough to secure-agent-setup.md, and
the doc's quick-start + the new section together cover the new
hook's install steps.
This is PR-3 of 3 addressing sandbox friction:
- PR-1 (#291) -- catalog + cross-links (merged).
- PR-2 (#292) -- setup-isolated-setup-doctor skill (open).
- PR-3 (this PR) -- PostToolUse hint hook.
Cross-references to the doctor skill (PR-2) will resolve once
#292 merges. The hook itself works independently of #292.
Generated-by: Claude Code (Opus 4.7)
---
.../skills/setup-isolated-setup-verify/SKILL.md | 20 ++-
docs/setup/sandbox-troubleshooting.md | 21 ++++
docs/setup/secure-agent-setup.md | 135 ++++++++++++++++++++-
tools/agent-isolation/README.md | 1 +
tools/agent-isolation/sandbox-error-hint.sh | 122 +++++++++++++++++++
5 files changed, 288 insertions(+), 11 deletions(-)
diff --git a/.claude/skills/setup-isolated-setup-verify/SKILL.md
b/.claude/skills/setup-isolated-setup-verify/SKILL.md
index d4382cd..2ccbb6b 100644
--- a/.claude/skills/setup-isolated-setup-verify/SKILL.md
+++ b/.claude/skills/setup-isolated-setup-verify/SKILL.md
@@ -104,15 +104,25 @@ Walk each in order:
1. Project `.claude/settings.json` shape — `sandbox.enabled: true`,
`permissions.deny`, `permissions.ask`, `sandbox.network.allowedDomains`.
2. User-scope `~/.claude/settings.json` wiring — `PreToolUse`
- `Bash` matcher → `sandbox-bypass-warn.sh`, `statusLine` →
+ `Bash` matcher → `sandbox-bypass-warn.sh`, `PostToolUse`
+ `Bash` matcher → `sandbox-error-hint.sh`, `statusLine` →
`sandbox-status-line.sh` (or a custom statusline script that
embeds the framework's prefix logic — that is the doc-allowed
- variant; report ⚠).
-3. Hook scripts present + executable — both
- `~/.claude/scripts/sandbox-bypass-warn.sh` and
+ variant; report ⚠). A missing `PostToolUse` entry for
+ `sandbox-error-hint.sh` reports ⚠ (not ✗) — the hook is a
+ discoverability aid for the failure modes catalogued in
+
[`docs/setup/sandbox-troubleshooting.md`](../../../docs/setup/sandbox-troubleshooting.md);
+ absence does not break anything, it just means an adopter
+ hitting one of those failures sees the raw error without the
+ `[sandbox-hint]` annotation.
+3. Hook scripts present + executable — all three of
+ `~/.claude/scripts/sandbox-bypass-warn.sh`,
+ `~/.claude/scripts/sandbox-error-hint.sh`, and
`~/.claude/scripts/sandbox-status-line.sh`. Symlinks into a
`~/.claude-config` sync repo are equivalent to direct files;
- resolve the link target and check that.
+ resolve the link target and check that. ⚠ (not ✗) for a
+ missing `sandbox-error-hint.sh`, with the same rationale as
+ check 2.
4. `claude-iso` shell function defined + sourced. The grep
pattern is the source line in `~/.bashrc` / `~/.zshrc`. Check
whether `alias claude='claude-iso'` is set; report it as a
diff --git a/docs/setup/sandbox-troubleshooting.md
b/docs/setup/sandbox-troubleshooting.md
index 59f9869..cdc4e82 100644
--- a/docs/setup/sandbox-troubleshooting.md
+++ b/docs/setup/sandbox-troubleshooting.md
@@ -43,6 +43,27 @@ If you hit a sandbox-shaped failure not listed below, add it
here
in the same shape — the catalog grows by experience, not by
prediction.
+Two surfaces make these entries discoverable in-session so a
+future reader does not have to remember the catalog exists:
+
+- The
[`setup-isolated-setup-doctor`](../../.claude/skills/setup-isolated-setup-doctor/SKILL.md)
+ skill probes each catalogued failure mode on demand and links
+ back to the matching entry. Invoke it when you suspect a
+ sandbox restriction; it runs the full probe set even when only
+ one is in question.
+- The
+ [Sandbox-error hint hook](secure-agent-setup.md#sandbox-error-hint-hook)
+ fires after every Bash tool call, pattern-matches the result
+ for the literal error strings catalogued below, and prints a
+ `[sandbox-hint] …` line pointing at the matching entry — so
+ the catalog reference appears next to the error automatically.
+
+When the catalog grows a new entry, extend both surfaces too:
+add a matching probe to the doctor skill, and add a matching
+`match … hint=…` branch to the hint hook. The catalog stays the
+source of truth; the doctor and the hook stay the discoverability
+layer.
+
Related:
- [`secure-agent-setup.md`](secure-agent-setup.md) — full install
diff --git a/docs/setup/secure-agent-setup.md b/docs/setup/secure-agent-setup.md
index 70eb146..54e5806 100644
--- a/docs/setup/secure-agent-setup.md
+++ b/docs/setup/secure-agent-setup.md
@@ -25,6 +25,12 @@
- [Install (user-scope)](#install-user-scope)
- [Verify](#verify)
- [Trade-offs](#trade-offs)
+ - [Sandbox-error hint hook](#sandbox-error-hint-hook)
+ - [Why install it](#why-install-it)
+ - [Why install it user-scope, not
project-scope](#why-install-it-user-scope-not-project-scope-1)
+ - [Install (user-scope)](#install-user-scope-1)
+ - [Verify](#verify-1)
+ - [Trade-offs](#trade-offs-1)
- [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)
@@ -156,11 +162,13 @@ npm install -g --no-save @anthropic-ai/[email protected]
# file, optionally alias `claude=claude-iso`. Section: "The
# clean-env wrapper" below.
-# 4. User-scope hooks. Copy `sandbox-bypass-warn.sh` and
-# `sandbox-status-line.sh` into `~/.claude/scripts/`, wire
-# them into `~/.claude/settings.json` under `PreToolUse` and
-# `statusLine`. Sections: "Sandbox-bypass visibility hook"
-# and "Sandbox-state status line" below.
+# 4. User-scope hooks. Copy `sandbox-bypass-warn.sh`,
+# `sandbox-error-hint.sh`, and `sandbox-status-line.sh` into
+# `~/.claude/scripts/`, wire them into `~/.claude/settings.json`
+# under `PreToolUse`, `PostToolUse`, and `statusLine`.
+# Sections: "Sandbox-bypass visibility hook",
+# "Sandbox-error hint hook", and "Sandbox-state status line"
+# below.
# 5. Verify the install actually denies what it claims to —
# section "Verification" below has both a three-line Bash
@@ -919,6 +927,121 @@ entirely) should produce no output and `exit=0`.
every Claude Code upgrade — same cadence as the
[Verification](#verification) section below.
+## Sandbox-error hint hook
+
+Companion to the *Sandbox-bypass visibility hook* above — a
+`PostToolUse` hook that fires **after** every Bash tool call and
+scans the result for the known sandbox-shaped error signatures
+catalogued in
+[`sandbox-troubleshooting.md`](sandbox-troubleshooting.md).
+On a match, prints a `[sandbox-hint] …` line to stderr pointing
+at the matching catalog entry. The tool's actual outcome is
+unchanged — the hook is purely an annotation layer that surfaces
+the catalog reference at the moment of failure, so the agent (or
+the user) does not have to remember the catalog exists.
+
+### Why install it
+
+The catalog (PR #291) and the diagnostic skill
+[`setup-isolated-setup-doctor`](../../.claude/skills/setup-isolated-setup-doctor/SKILL.md)
+(PR #292) cover the same ground but require explicit
+recall — *"my SSH push failed; let me check the catalog"* or
+*"let me run the doctor"*. The hint hook closes the loop by
+making the catalog reference appear next to the error
+automatically. Three classes of failure are recognised today:
+
+| Error signature | Catalog anchor |
+|---|---|
+| `Could not open a connection to your authentication agent` / `agent refused
operation` / `ssh-add: error fetching identities` / `Permission denied
(publickey)` | [SSH agent / Yubikey
unreachable](sandbox-troubleshooting.md#ssh-agent--yubikey-appears-unreachable-from-inside-the-sandbox)
|
+| `Cannot connect to the Docker daemon` / `open /var/run/docker.sock:
operation not permitted` / `Cannot connect to Podman` / podman `connect:
permission denied` | [Docker / Podman socket
denied](sandbox-troubleshooting.md#docker--podman-command-fails-with-a-socket-error)
|
+| `127.0.0.1 … Permission denied` / `Operation not permitted … bind` / `Errno
49 … assign requested address` / `Connection refused … 127.0.0.1` | [Localhost
port-bind
blocked](sandbox-troubleshooting.md#test-cannot-bind-to-a-localhost-port) |
+
+The hint also tells the user to run
+`/setup-isolated-setup-doctor` for a structured probe of all
+three failure modes, so a single mid-flow failure can lead to a
+broader sandbox health-check.
+
+### Why install it user-scope, not project-scope
+
+Same reasoning as the bypass-warn hook: the failure signatures
+the hook detects are not framework-specific — they show up in any
+sandboxed Bash session against any project. Putting the hook in
+`~/.claude/settings.json` makes the hint fire across every
+project on the host, including adopters that have not (yet)
+adopted the framework. Project-scope wiring would leave
+unrelated sessions silent.
+
+### Install (user-scope)
+
+```bash
+mkdir -p ~/.claude/scripts
+cp /path/to/airflow-steward/tools/agent-isolation/sandbox-error-hint.sh \
+ ~/.claude/scripts/sandbox-error-hint.sh
+chmod +x ~/.claude/scripts/sandbox-error-hint.sh
+```
+
+Then wire under `PostToolUse` with a `Bash` matcher. If a
+`PostToolUse` `Bash` matcher already exists for another hook,
+append to its `hooks` array rather than creating a second
+matcher block:
+
+```jsonc
+{
+ "hooks": {
+ "PostToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "~/.claude/scripts/sandbox-error-hint.sh"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+### Verify
+
+The hook is exit-code-driven — exit 1 with stderr output means
+"surface stderr to the user as a tool-result hint". To test
+without a real failure:
+
+```bash
+echo '{"tool_name":"Bash","tool_response":{"stdout":"","stderr":"Could not
open a connection to your authentication agent."}}' \
+ | ~/.claude/scripts/sandbox-error-hint.sh; echo "exit=$?"
+```
+
+Expected: a yellow `[sandbox-hint] SSH agent / Yubikey appears
+unreachable …` line on stderr, then `exit=1`. A second call with
+benign tool output (e.g. `"stdout":"hello world","stderr":""`)
+should produce no output and `exit=0`.
+
+### Trade-offs
+
+- **Pattern-matched, not semantic.** The hook recognises literal
+ error strings; it does not know *why* a tool call failed. A
+ failure mode dressed up in a userland framework's generic error
+ ("test failed", "build error") slips past silently. The
+ doctor skill is the catch-all when the hint does not fire and
+ the user suspects a sandbox issue.
+- **Pattern set must stay in lock-step with the catalog.** When a
+ new entry lands in
[`sandbox-troubleshooting.md`](sandbox-troubleshooting.md),
+ add a matching `match … hint=…` branch to the script. The
+ catalog is the source of truth; the hook is the discoverability
+ layer.
+- **Fail-open by design.** Any unexpected JSON shape, missing
+ `tool_response`, missing `jq`, or other parse failure exits 0
+ silently. A broken hint must never break a legitimate tool
+ call. Cost: a future Claude Code hook-schema change can silently
+ stop the hook from firing; re-run the verification snippet
+ above after every Claude Code upgrade.
+- **Non-blocking.** The hook exits 1, not 2 — the tool call
+ result is unchanged. The hint is informational; the user
+ decides whether to apply the catalog's remediation.
+
## Sandbox-state status line
The Claude Code terminal footer (`statusLine`) is the
@@ -1052,7 +1175,7 @@ paths). Track the artifacts you want shared, symlink them
into
| 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 |
+| `scripts/sandbox-bypass-warn.sh`, `scripts/sandbox-error-hint.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 |
diff --git a/tools/agent-isolation/README.md b/tools/agent-isolation/README.md
index fb5c015..b6e93f1 100644
--- a/tools/agent-isolation/README.md
+++ b/tools/agent-isolation/README.md
@@ -29,6 +29,7 @@ versions.
| [`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-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`. |
| [`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 [...]
diff --git a/tools/agent-isolation/sandbox-error-hint.sh
b/tools/agent-isolation/sandbox-error-hint.sh
new file mode 100755
index 0000000..050c3cf
--- /dev/null
+++ b/tools/agent-isolation/sandbox-error-hint.sh
@@ -0,0 +1,122 @@
+#!/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-error-hint.sh — Claude Code PostToolUse hook (Bash matcher).
+#
+# After every Bash tool call, scan stdout + stderr for the literal
+# error strings the sandbox produces when it blocks a legitimate
+# workflow (SSH agent socket unreachable, loopback port blocked,
+# docker / podman socket denied). On a match, emit a one-line
+# `[sandbox-hint] …` to stderr pointing at the matching entry in
+# `docs/setup/sandbox-troubleshooting.md` — so the agent (and the
+# user) sees the catalog reference at the moment of failure,
+# without having to remember the catalog exists.
+#
+# Recommended placement: user-scope `~/.claude/settings.json`, so
+# the hint fires across every session on the host. The hook does
+# NOT modify the tool's behaviour — the tool call still failed;
+# this is purely an annotation layer pointing at the fix.
+#
+# Behaviour:
+# - Reads the PostToolUse JSON envelope on stdin.
+# - Filters to `tool_name == "Bash"`; everything else exits 0
+# silently (the patterns below are Bash-stderr-shaped).
+# - Extracts `tool_response.stdout + tool_response.stderr`. Falls
+# back to treating `tool_response` as a plain string if the
+# object form is not present (defensive against Claude Code
+# hook-schema changes).
+# - Greps the combined output for known sandbox-shaped signatures
+# (catalogued in `docs/setup/sandbox-troubleshooting.md`).
+# - On match, prints a `[sandbox-hint] …` line to stderr and exits
+# 1 (a non-zero exit that is *not* 2 surfaces stderr to the
+# user / model as a tool-result annotation; exit 2 would block
+# the call retroactively, which is wrong — the tool already
+# ran).
+# - On no match, on any JSON-parse failure, or on any unexpected
+# input shape, exits 0 silently. The hook is intentionally
+# fail-open: a broken hint should never break a legitimate tool
+# call.
+#
+# Wiring (user-scope, applies to every session on the host):
+#
+# {
+# "hooks": {
+# "PostToolUse": [
+# {
+# "matcher": "Bash",
+# "hooks": [
+# { "type": "command",
+# "command": "~/.claude/scripts/sandbox-error-hint.sh" }
+# ]
+# }
+# ]
+# }
+# }
+#
+# See `docs/setup/secure-agent-setup.md` → "Sandbox-error hint hook"
+# for install steps, the trade-offs, and the relationship with the
+# `setup-isolated-setup-doctor` skill (the doctor is the in-session
+# diagnostic; this hook is the just-in-time hint).
+
+set -u
+
+input=$(cat)
+
+# Filter to Bash tool calls. Everything else passes through silent.
+tool_name=$(printf '%s' "$input" | jq -r '.tool_name // ""' 2>/dev/null ||
echo "")
+[ "$tool_name" = "Bash" ] || exit 0
+
+# Extract combined stdout + stderr from the tool response. Object
+# shape is the current Claude Code contract; string-shape fallback
+# is defensive for older / future schema variants.
+output=$(printf '%s' "$input" | jq -r '
+ if (.tool_response | type) == "object" then
+ (.tool_response.stdout // "") + "\n" + (.tool_response.stderr // "")
+ else
+ (.tool_response // "")
+ end
+' 2>/dev/null || echo "")
+
+[ -n "$output" ] || exit 0
+
+# Pattern set. Each entry is (literal regex against output, anchor
+# in docs/setup/sandbox-troubleshooting.md). Keep the regex
+# specific — fail-open is fine, false-positive hints are noise.
+hint=""
+doc_path="docs/setup/sandbox-troubleshooting.md"
+
+match() { printf '%s' "$output" | grep -qE "$1"; }
+
+if match 'Could not open a connection to your authentication agent|agent
refused operation|ssh-add: error fetching identities for protocol|Permission
denied \(publickey\)'; then
+ hint="SSH agent / Yubikey appears unreachable from inside the sandbox. See
${doc_path}#ssh-agent--yubikey-appears-unreachable-from-inside-the-sandbox"
+elif match 'Cannot connect to the Docker daemon|open /var/run/docker\.sock:
operation not permitted|Cannot connect to Podman|connect: permission
denied.*podman\.sock'; then
+ hint="Docker / Podman runtime socket denied by the sandbox. See
${doc_path}#docker--podman-command-fails-with-a-socket-error"
+elif match '127\.0\.0\.1.*[Pp]ermission denied|[Oo]peration not
permitted.*bind|Errno 49.*assign requested address|HTTP.*Connection
refused.*127\.0\.0\.1'; then
+ hint="Localhost port-bind or loopback HTTP may be sandbox-blocked. See
${doc_path}#test-cannot-bind-to-a-localhost-port"
+fi
+
+[ -n "$hint" ] || exit 0
+
+esc=$(printf '\033')
+yellow="${esc}[1;33m"
+reset="${esc}[0m"
+
+printf '%s[sandbox-hint]%s %s\n' "$yellow" "$reset" "$hint" >&2
+printf '%s %s Run %s/setup-isolated-setup-doctor%s for a
structured probe of all three failure modes.\n' "$yellow" "$reset" "${esc}[1m"
"${esc}[0m" >&2
+
+exit 1