Main-thread time guard for Claude Code Bash commands: catches foreground polling and watching, and slow commands about to be killed by an inadequate timeout.
Highlights
A target-aware slow-command form: register "make": {"e2e*": 1800000} instead of writing a regex. The command word must equal the registered name (compared by basename, so /usr/bin/make counts) and the glob must match a whole argument word — no substring matching, so a quoted argument is one token and NOTE="a note mentioning e2e somewhere" can never fire. That closes the leak the regex form invites: an unbounded .* in a pattern like make .*\be2e reaches past the command word into quoted arguments, and the v0.4.0 command-position anchoring can't help because make genuinely is in command position (#21). Target maps merge additively per command across config files, same as the rest of the registry, and a broken entry still loses only itself.
The .* footgun is now documented where you'd hit it. The README's Class B section covers both registration forms, recommends the target form for command-plus-argument registrations, and warns that .* in the regex form puts quoted arguments in scope. The reduce-foreground-guard-prompts skill points regex-.* false positives at the target form.
cut-release.sh no longer reports failure on a successful release. The EXIT-trap bug the v0.4.0 notes flagged is fixed: with --notes-file, the cleanup handler's short-circuited && leaked a non-zero status into the script's exit code after every artifact had published correctly. Release tooling only — nothing in the installed plugin changes.
Upgrading
Install or update as usual — nothing is required, and no config file needs to change. Both slow-command forms coexist in one commands dict, and existing regex entries keep working unchanged.
Optional: a regex entry you prefixed with .* during the v0.4.0 migration so it could match an argument is a candidate for the target form, which cannot leak into quoted arguments and has no regex to get wrong.
Nothing is deprecated or removed in this release. No config key changed name, default, or meaning.
Configuration surface
No keys were added or removed — the table in Configuration still has the same keys as v0.3.0. slow.commands gained a second, additive value shape: alongside the regex form "<pattern>": <minimum timeout ms>, an entry may now be "<command name>": {"<argument glob>": <minimum timeout ms>}. The Class B reason for the new form reads matches the slow-command target `make e2e*` , and the friction report buckets both wordings under the same slow-timeout category.
Everything since v0.4.0
Two PRs.
- feat(class-b): add a target-aware slow-command form by @karlkfi in #23 (fixes #21)
- fix(release): stop the EXIT trap from failing a successful release by @karlkfi in #20
Validation
152 tests, green across Python 3.10, 3.11, 3.12, and 3.13 on the commit this release is cut from (run) — 135 at v0.4.0. The new coverage: 13 end-to-end tests over the target form (SlowTargetTests), including both reproduction commands from #21 asserted as defers; friction-report coverage of the new reason wording; and the first tests over scripts/cut-release.sh, which lift the EXIT-trap code verbatim out of the shipped script so the assertion tracks the source rather than a copy.
The never-allow invariant is asserted on every end-to-end call: the hook returns ask, deny, or nothing. An allow would bypass your permission settings and override the sibling guards evaluating the same command.
What the suite does not assert
- No live Claude Code session runs in CI. Hook behaviour is asserted against synthetic payloads, not a running CLI.
scripts/cut-release.shstill has no end-to-end test — its preflight runs the unit suite, which would recurse — so only the EXIT-trap regression from #20 is covered.- Class B's registry is per-repo by design, so no test can tell you whether your slow commands are registered — only that a registered one behaves. The friction report is the tool for that.
Security
No security advisory accompanies this release, and no dependency changed — the hook remains a single stdlib-only Python script with no telemetry; the friction report re-reads your local session transcripts and sends nothing anywhere. See PRIVACY.md.
This is a productivity guard, not a security boundary. Parsing uncertainty defers, and infrastructure errors fail open to a silent defer, because a guard that breaks the session is worse than a missed prompt. Do not deploy it as a control that something must not run.
Installing this release
/plugin marketplace add karlkfi/claude-foreground-guard
/plugin install foreground-guard@foreground-guard
Note
An existing install does not pick this up on its own. Claude Code auto-updates official Anthropic marketplaces only; a third-party GitHub marketplace stays pinned at the version you installed. Either set autoUpdate for the marketplace in ~/.claude/settings.json once, or update by hand — both are in Keeping it updated.
claude plugin marketplace update foreground-guard
claude plugin update foreground-guard@foreground-guard
Restart Claude Code (or /reload-plugins) to apply. Requires python3 on your PATH.
The plugin pairs with the sibling guards — workspace-guard, branch-guard, and prod-guard — which is why this one never emits allow.
Full changelog: v0.4.0...v0.5.0