When the auto-mode classifier blocks a Bash command, ask you Run or Don't run instead of having Claude tell you to run it with !

Six Claude Code mods that guard risky actions and cut down on babysitting: confirm production changes, keep secrets out of the model's context, start branches from the right base, hold Claude to "just look" when you ask for it, tell you when CI finishes, and let you approve a command auto mode blocked with one button.
They started as one person's setup and stay opinionated, but every personal value is a plugin option with a sensible default.
[!WARNING] Mods run inside Claude Code with your permissions and are not sandboxed. These guards parse shell commands heuristically: they catch the common forms Claude writes, not every way a command can be spelled. Treat them as a safety net for mistakes, not a security boundary. Read the source before installing.
| Mod | What it does |
|---|---|
prod-guard | Holds mutating kubectl/helm commands against production contexts and aws iam writes until you confirm the exact command; a confirmed call skips the auto-mode classifier, and one the classifier blocks anyway goes into your prompt box as ! …. Refuses mutating kubectl without an explicit --context. Judges the command inside aws-vault exec <profile> -- … by the same rules, and puts aws-vault login or a bare aws-vault exec <profile> (an interactive shell) in your prompt box as ! … instead of running it (left out when the box holds a draft, so the draft is kept). |
secret-guard | Blocks Bash and Read access to .env files (not .env.example) and to ~/.claude.json. Masks JWTs, AWS/GitHub/Slack/Anthropic/OpenAI/Google keys and private key blocks in Bash and Read output before Claude sees them. |
branch-base | Rewrites git checkout -b, git switch -c and git worktree add with no start point to start from origin/<base> after a fetch, and adds --base <base> to gh pr create. When a repo has no such branch, asks you which base to use (remembered per repo until reload). |
read-only-lock | After a look-only prompt ("just investigate", "plan first, don't change anything", or Korean equivalents such as "확인만 해"), blocks file edits and mutating commands and shows a one-line band with an Unlock button. "go ahead", "apply it" or the button unlocks. |
deploy-watch | After git push or gh pr create, polls GitHub Actions for that commit every 30 s; after gh pr merge, for the merge commit GitHub made on the base branch, where deploy workflows run (an auto-merge is followed until it lands); after gh workflow run, for the dispatched workflow. Raises a toast and a notification when the runs finish and sends the result to Claude as a message that starts a turn (a push that started no runs is only noted, without starting one), so Claude reports it and picks up work that was waiting on CI without watching CI itself. /ci opens a pane with the runs. |
approve-blocked | When the auto-mode classifier blocks a Bash command, asks you Run or Don't run in a dialog instead of leaving Claude to tell you to run it with !. Run runs the call again past the classifier (a permissions.deny rule still stands); if the classifier blocks it again, the command goes into your prompt box as ! … so Enter runs it. Don't run tells Claude not to hand the command to you. A call prod-guard already had you confirm is not asked about twice. |
Requires Claude Code v2.1.287 or later (mods are on by default). At the prompt of a terminal session:
/plugin install prod-guard --marketplace blackironj/claude-mods
Answer y to add the marketplace, then pick a scope (user scope loads it in every session). Repeat for each mod you want: secret-guard, branch-base, read-only-lock, deploy-watch, approve-blocked.
From a shell:
claude plugin install prod-guard --marketplace blackironj/claude-mods
Disable or uninstall any mod from the Installed tab in /plugin.
Set them on the install screen, or change them later in /config.
| Mod | Option | Default | Meaning |
|---|---|---|---|
prod-guard | prodContexts | prod | Comma-separated kube contexts whose mutations need confirmation (also matched against helm --kube-context). |
prod-guard | requireContext | true | Refuse mutating kubectl commands without --context. |
branch-base | baseBranch | dev | Remote branch new branches start from and PRs target. |
deploy-watch needs the GitHub CLI signed in; branch-base needs git.claude -p and the VS Code chat panel the guards still run but nothing is drawn.read-only-lock decides from keywords, so it can lock or stay unlocked when you didn't mean it to; the band shows when it is on.secret-guard also blocks commands that create a .env (for example cp .env.example .env); run those yourself. Masking covers the token shapes listed above, not arbitrary passwords.branch-base does not affect worktrees Claude Code creates on its own (EnterWorktree).approve-blocked turns every auto-mode block of a Bash command into a question for you, so pressing Run overrides the classifier for that one call: read the command first. Other tools the classifier blocks are left as they are.prod-guard lets a call you confirmed run even where a permission rule or the auto-mode classifier would stop it, and its aws-vault handoff puts a command Claude wrote into your prompt box: a ! command runs in your shell with no permission check, so read it before pressing Enter.prod-guard lets aws-vault exec <profile> -- … run in Claude's shell, which has no terminal for prompts. When the session needs an MFA code or a key-store passphrase, the command fails; run one aws-vault command yourself with ! to refresh the session.Each mod is a plugin folder (.claude-plugin/plugin.json, hooks/hooks.json, a TypeScript hooks module, tests). There is no build step: Claude Code loads the .ts/.tsx sources directly. See CLAUDE.md for the layout, checks and conventions.
claude plugin validate prod-guard # what the module hooks and calls, and anything the engine would refuse
claude plugin test prod-guard # runs prod-guard/tests/*.test.ts against the engine
claude --plugin-dir ./prod-guard # a session with your working copy loaded and hot-reloaded
hooks/register.ts 70 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { classifierBlock } from './blocked'
4
5const MAX_SHOWN = 600
6
7/** Puts `! <command>` in an empty prompt box and tells the model to hand the command to the user. */
8async function handOff($: EngineInterface, command: string, why: string) {
9 // Fill only an empty prompt box: replacing would throw away what the user was typing.
10 const isFilled = (await $.prompt.read()).text.trim() === '' && (await $.prompt.fill({ text: `! ${command}`, mode: 'replace' })).isFilled
11 $.ui.toast(isFilled ? 'approve-blocked: press Enter in your prompt box to run the command' : 'approve-blocked: run the command yourself with !')
12 return {
13 deny:
14 `approve-blocked: ${why}, so it was handed to the user` +
15 (isFilled ? ' (placed in their prompt box as a `!` command)' : '') +
16 '. Do not retry or work around it. End your turn now with no further tool calls, and show the user ' +
17 'this exact line to run, in a code block, `!` included:\n\n' +
18 `! ${command}\n\n` +
19 'Its output lands in this conversation when they run it.',
20 }
21}
22
23export const register: Register = on => {
24 // Commands the user approved after a block, while their retry runs, so tool.check can let them past the classifier.
25 const approved = new Set<string>()
26
27 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
28 const result = await next(e)
29 const reason = classifierBlock(result)
30 if (reason === undefined) return result
31
32 // Another guard (prod-guard) already had the user approve this call: don't ask twice.
33 if ((await $.tool.check({ tool: 'Bash', input: { command: e.command } })).decision === 'allow') {
34 return handOff($, e.command, `auto mode blocked it although the user had approved it (${reason})`)
35 }
36
37 const shown = e.command.length > MAX_SHOWN ? `${e.command.slice(0, MAX_SHOWN)}…` : e.command
38 const answer = await $.ui.ask(`Auto mode blocked this command (${reason}). Run it anyway?\n\n${shown}`, {
39 header: 'Blocked',
40 options: ['Run', "Don't run"],
41 })
42 if (answer !== 'Run') {
43 return {
44 deny:
45 `approve-blocked: auto mode blocked the command and the user chose not to run it${answer === "Don't run" ? '' : `: ${answer}`}. ` +
46 'Do not ask them to run it themselves with `!`; go on without it, or ask what to do instead.',
47 }
48 }
49 approved.add(e.command)
50 try {
51 const retry = await next(e)
52 const again = classifierBlock(retry)
53 return again === undefined ? retry : await handOff($, e.command, `auto mode blocked it again after the user chose Run (${again})`)
54 } finally {
55 approved.delete(e.command)
56 }
57 }).catch(($, e, next) =>
58 next.called ? next(e) : { deny: 'approve-blocked: the hook failed; the command did not run.' },
59 )
60
61 on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
62 const verdict = await next(e)
63 const command = (e.input as { command?: string }).command ?? ''
64 // Only the classifier's question is answered for the user: a settings deny rule still stands.
65 return approved.has(command) && verdict.decision === 'ask'
66 ? { decision: 'allow', reason: 'approve-blocked: approved by the user after an auto-mode block' }
67 : verdict
68 })
69}
70hooks/blocked.ts 11 lines1import type { ToolCallResult } from 'claude-code'
2
3const CLASSIFIER_BLOCK = /denied by the Claude Code auto mode classifier/
4
5/** The classifier's reason when a call's result is an auto-mode block, else undefined. */
6export function classifierBlock(result: ToolCallResult): string | undefined {
7 const text = result.deny ?? (result.isError === true ? result.text : undefined)
8 if (text === undefined || !CLASSIFIER_BLOCK.test(text)) return undefined
9 return /Reason: (.+?)\. If you have/s.exec(text)?.[1] ?? 'no reason given'
10}
11