reduce-foreground-guard-prompts · git:20260822.35bc521 · 2026-08-22 · sha256 55c1458846d674a5

reduce-foreground-guard-prompts git:20260822.35bc521A

Immutable. This exact content is served forever at /api/v1/blob/55c1458846d674a5.

---
name: reduce-foreground-guard-prompts
description: Explain why foreground-guard is prompting on Bash commands and how to stop the avoidable prompts. Use when the user asks "why am I getting so many foreground-guard prompts", "reduce foreground-guard prompts", "stop the watch/sleep/gh run watch permission prompts", or otherwise wants fewer confirmation prompts from this hook.
---

# Reducing foreground-guard prompts

foreground-guard is a `PreToolUse` hook for `Bash` that guards the session's
**main-thread time**. It blocks in two classes and defers silently on
everything else. The verdict is `deny` by default: the resource at stake is the
session's own time, so every finding carries a rewrite the agent can apply and
nobody has to be woken. `"action": "ask"` on either class turns the denies back
into prompts for someone who wants to watch the guard work.

- **Class A — foreground poll/watch**: a command that parks the main thread on a
  live view — watch/follow modes (`gh run watch`, `gh pr checks --watch`,
  `kubectl logs -f`, `kubectl get -w`, `tail -f`, `journalctl -f`,
  `docker logs -f`, `watch ...`), a `while`/`until`/`for` loop that polls with
  `sleep`, a chained repeat-with-sleep (`cmd; sleep N; cmd`), or a bare `sleep N`
  at/above the floor (default 10s). `run_in_background: true` does **not** exempt
  Class A: a detached poll still holds a task slot for the whole wait and hands
  back output whose freshness the agent can't judge, so it still blocks.
- **Class B — slow command with an inadequate timeout**: a command the repo
  registered as slow that is about to run in the foreground with the Bash call's
  `timeout` below the registered minimum — it would be killed mid-run.
  `run_in_background: true` **is** the fix here, and exempts it.

So a flood of prompts almost always means the agent keeps waiting on a poll
instead of snapshotting and re-checking next turn, or keeps under-timing a
known-slow command — both fixable habits — not that the work genuinely needs to
block.

## Diagnose

Don't guess about past friction — measure it. The plugin ships an analyzer,
`scripts/friction-report.py`, that re-reads the hook decisions Claude Code
already recorded in the local session transcripts and ranks them by category,
flagged tool, and triggering command (no telemetry — see PRIVACY.md). Run it
first so the diagnosis is grounded in the user's real prompt history:

```
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/friction-report.py" --repo "$(basename "$CLAUDE_PROJECT_DIR")"
```

This reports the prompt count and the share of the window's Bash calls it
represents (plus how many sessions saw a prompt), a **By category** breakdown,
**By flagged tool** and **Top flagged targets** rankings (the watch commands and
slow patterns being hit), and the **Top triggering commands**. A **Hook
failures** section appears only when a hook crashed or timed out; that is a guard
not running at all rather than friction, so report it as-is instead of tuning
config around it. Useful adjustments:

- `--since 24h` / `--since 2026-06-01` / `--since all` — widen or narrow the
  window (default `7d`).
- `--repo ''` — drop the project filter to see friction across every repo.
- `--plugin all` — include the sibling guards' decisions too (prod-guard,
  workspace-guard, branch-guard), if the user wants the whole picture. The
  override count is foreground-guard's own, so it is omitted in this
  mode; read it from the default single-guard report instead. Denies are
  recovered from tool-result text, so a sibling whose reason does not open
  `<name>-guard:` under-counts them — don't read a low deny count for another
  guard as that guard rarely blocking.
- `--json` — machine-readable, if you'd rather parse it than read the table.

**Fall back gracefully.** If the script can't be found (`$CLAUDE_PLUGIN_ROOT`
unset — try the in-repo path `scripts/friction-report.py`), exits with "No
transcripts …", or prints "No … decisions found" (a fresh setup
with no recorded prompts yet), skip the data step and diagnose from the **most
recent foreground-guard prompts in this session** instead — each prompt's reason
text names the offending command and the fix. With neither, walk the user through
the category → fix map below against the commands they say keep prompting.

## Map categories to fixes

The report's category names are a stable contract; each maps to one fix. Tell the
user which categories dominate their report, then apply the matching fix:

1. **`watch`** (Class A) — a live watch/follow mode. **Reason:** "…runs in
   watch/follow mode…". Fix the behavior: take **one** non-blocking snapshot
   (`gh pr checks <pr>` without `--watch`, `gh run view <id>`, `tail -n 100`,
   `kubectl logs --tail=100`, `kubectl get` once) instead of streaming, and
   re-check next turn, or arm a Monitor that exits when the run reaches a
   terminal state. Re-running the same call with `run_in_background: true`
   does not quiet this category — the watch still occupies a task slot and the
   guard still prompts. If the flagged command is a **false positive** — a form
   you genuinely want to run live and don't want prompted — add a
   `poll.exempt_watch_patterns` regex (exemptions win over matches; this quiets
   just that form without disabling all of Class A). Conversely, if a real watch
   reached through an uncovered alias (`k logs -f …`) is *not* being caught but
   should be, that's a coverage gap → `poll.extra_watch_patterns`.
2. **`loop-sleep`** (Class A) — a `while`/`until`/`for` loop that polls with
   `sleep`. **Reason:** "…loop with `sleep` polls…". Fix the behavior: take one
   status check now and check again next turn — don't spin a poll loop, on the
   main thread or in the background (a backgrounded loop still prompts).
3. **`sandwich`** (Class A) — a chained repeat-with-sleep (`cmd; sleep N; cmd`).
   **Reason:** "…repeat-with-sleep chain…". Same fix as `loop-sleep`: one check
   now; defer the recheck to the next turn.
4. **`bare-sleep`** (Class A) — a long bare `sleep N` at/above the floor.
   **Reason:** "…parks the main thread for…" (backgrounded: "…parks a background
   task for…"). Skip the wait and do the follow-up check now (`sleep 300 && curl
   …` → just run the `curl` next turn). If the flagged sleeps are legitimately
   *short* startup-grace waits that sit just above the floor, raise
   `poll.sleep_floor_seconds` so they fall below it — but keep the floor low
   enough that real long waits still prompt.
5. **`slow-timeout`** (Class B) — a registered slow command about to be killed by
   an inadequate timeout. **Reason:** "…matches the slow-command pattern…" or
   "…matches the slow-command target…". Set an adequate `timeout:` on the Bash
   call (the reason names the minimum in ms), or run it with
   `run_in_background: true`. If a command is flagged slow but is *not* actually
   slow anymore, remove or lower its entry in `slow.commands`. If a **regex**
   entry with `.*` is firing on commands that merely *mention* the registered
   word in an argument, rewrite it in the target form —
   `"make": {"e2e*": 1800000}` — which matches whole argument words only.

The **Top flagged targets / commands** rankings tell you *which* commands to
target first — fix the highest-count rows for the biggest reduction. If the report
shows `FOREGROUND_GUARD_OVERRIDE prefixes`, an override is being leaned on
routinely; that command is a good candidate for a real fix (snapshot it, or arm a
Monitor) rather than a per-run override. The count is read off the commands
themselves, so it includes overrides pasted onto calls the guard would never have
blocked — a stale habit shows up here.

## Fix

Tell the user the cause(s) you found, then apply the habits that prevent them:

- **Don't wait on a poll at all — snapshot and re-check next turn.**
  Backgrounding a poll is not the fix: `run_in_background: true` only exempts
  Class B (a registered slow command), where it is exactly what the guard wants.
- **When the wait is unavoidable, arm a Monitor.** A Monitor whose script exits
  once the condition flips wakes the session with a dated event and holds no
  Bash task slot — the wait every Class A reason points at.
- **Take one snapshot, not a live stream.** `gh pr checks <pr>` (no `--watch`),
  `gh run view <id>`, `tail -n 100`, `kubectl logs --tail=100`, `kubectl get`
  once. Re-check on the next turn if you need fresher state.
- **Bound a deliberate wait with `timeout N …`.** A `timeout`-wrapped command is
  exempt from Class A (an explicit bound is the fix the guard teaches). Use it
  when you truly need to block briefly.
- **Set an adequate `timeout:` on known-slow Bash calls** so Class B stays quiet —
  or register the command in `slow.commands` so future under-timed runs get the
  reminder before they're killed.

Config lives in `.claude/foreground-guard.json` (per-repo) or
`~/.claude/foreground-guard.json` (user-level). The knobs that reduce prompts:

| Want to… | Knob |
| --- | --- |
| Quiet a specific built-in watch form that's a false positive | `poll.exempt_watch_patterns` (allowlist regexes over the command segment) |
| Stop short startup-grace sleeps from prompting | raise `poll.sleep_floor_seconds` (default 10) |
| Stop a slow command being flagged after it got fast | remove/lower its `slow.commands` entry |
| Watch the guard work instead of letting the agent self-correct | `poll.action: "ask"` / `slow.action: "ask"` — costs you a prompt per finding |
| Add repo-specific context to Class A prompts | `hint` (e.g. name your own PR-watcher machinery) |

For a genuinely-intentional one-off foreground wait, prefix the command with
`FOREGROUND_GUARD_OVERRIDE=<why> …` and the guard defers — in every permission
mode, including the ones where no prompt can be answered. Deferring is not
allowing: normal permissions and the sibling guards still see the call. The
stated reason stays on the command in the transcript, which is what the report
counts.

**Don't suggest disabling the guard wholesale** (`poll.enabled: false`,
`slow.enabled: false`, or `FOREGROUND_GUARD_DISABLE=1`) to silence legitimate
prompts — a real foreground poll is friction worth keeping. Reach for those only
when the harness itself has subsumed the behavior.

## Make it stick

Offer to paste the playbook below into the user's `CLAUDE.md` (or `AGENTS.md`) so
future sessions follow these habits from the start — the guard can only attach
advice to a prompt, so habits that avoid the prompt entirely have to live in
project guidance. Only do so with the user's go-ahead.

```markdown
## Avoiding foreground-guard prompts

This repo uses foreground-guard, a hook that guards the session's main-thread
time. It prompts before a Bash call parks the main thread on a foreground wait or
runs a known-slow command that its timeout would kill. To keep work flowing:

- **Don't watch — snapshot.** Instead of streaming `gh run watch`, `gh pr checks
  --watch`, `kubectl logs -f`, `kubectl get -w`, `tail -f`, `journalctl -f`,
  `docker logs -f`, or `watch …`, take one non-blocking reading and check again
  next turn. `run_in_background: true` does not make a poll acceptable — it
  parks the same wait in a task slot and returns output you can't date.
- **Take one snapshot, not a live stream.** Prefer `gh pr checks <pr>` (no
  `--watch`), `gh run view <id>`, `tail -n 100`, `kubectl logs --tail=100`, and a
  single `kubectl get`. Re-check next turn if you need fresher state.
- **Don't poll with `sleep`.** Avoid `while true; do …; sleep N; done` loops and
  `cmd; sleep N; cmd` repeat-with-sleep chains. Take one status check now and
  defer the recheck to the next turn.
- **Don't block on a bare `sleep N`.** Do the follow-up check on the next turn
  instead. A short startup-grace `sleep` below the floor (default 10s) is fine.
- **Wait on a Monitor, not a Bash call.** When you truly need to be woken by a
  condition flipping elsewhere, arm a Monitor whose script exits at that point:
  the event arrives dated and no Bash task slot is held for the wait.
- **Bound a deliberate wait with `timeout N …`.** An explicit bound is exempt —
  and the Bash tool's own timeout still backstops it.
- **Set an adequate `timeout:` on known-slow Bash calls** (test suites, e2e runs,
  `-race` builds) so they aren't killed by the default 2-minute timeout — or run
  them with `run_in_background: true`.
```

The plugin also ships the **`/foreground-guard:friction-report`** slash command
for the "just show me the numbers" case — it runs the analyzer directly and prints
the ranked report with no diagnosis. It passes its arguments straight through, so
the same flags work:

```
/foreground-guard:friction-report                      # last 7 days
/foreground-guard:friction-report --since 24h --repo gateway
/foreground-guard:friction-report --json
```