Releases: karlkfi/claude-prod-guard
Release list
v2.5.0
Production-target guard rails for Claude Code Bash commands.
Note
The guard itself did not change this release. scripts/bash-prod-guard.py and hooks/hooks.json are byte-identical to v2.4.1 — verified by diff at both tags, not asserted from memory. Every change below is in the optional /prod-guard:friction-report command.
Highlights
The friction report now flags a stale install. prod-guard installs from a third-party marketplace, and Claude Code auto-updates official Anthropic marketplaces only — so the version you installed stays pinned until you act. For a guard plugin that pin has teeth: a stale classifier is missing later false-negative fixes, not just conveniences. The report is where you already look when the guard annoys you, so it is where the lag now shows:
⚠ prod-guard 1.1.0 installed, 2.4.1 available in the local marketplace clone.
That is not a hypothetical version pair. The author's own machine ran 1.1.0 while the repo shipped 2.x, a full major version behind, missing the shell-variable-expansion classifier and more.
Important
The comparison is local-only, so no warning means "no lag against the clone you have" — not "up to date". It reads the marketplace clone on disk, never GitHub. If it has been a while, run claude plugin marketplace update prod-guard first, then re-run the report.
A 23k all-guard count no longer reads as 23k prod-guard prompts. Under --plugin all the header still said "prod-guard decisions analyzed" while the total spanned every guard installed. The label now follows the scope actually counted — all-guard for all, the guard's own name otherwise — in both the totals header and the no-results line.
Upgrading
No action required, and nothing to re-vet. No hook decision, verb table, or covered tool moved, so a config you tuned against v2.4.1 behaves identically here.
claude plugin marketplace update prod-guard
claude plugin update prod-guard@prod-guardRestart to apply. Both commands work headlessly and share Claude Desktop's plugin state, so they update a Desktop install too. Better still, set autoUpdate for the marketplace once and stop doing this by hand — see Keeping it updated, which is also what the new warning links you to.
Everything since v2.4.1
Two product changes. Both land in scripts/friction-report.py.
New features (1)
Fixes (1)
New configuration (1)
Nothing here is required; it defaults to the previous behaviour.
--plugins-dir(default~/.claude/plugins) — points the staleness check at a different plugins directory. Exists mostly so the end-to-end tests never touch a developer's real~/.claude/plugins.
Validation
410 tests pass, up from 389 at v2.4.1 — all 21 new ones covering the staleness check and the scope label. Stdlib unittest, no third-party dependencies, run on Python 3.10 through 3.13 in CI:
python3 -m unittest discover testsThe staleness check degrades rather than breaks: a missing, unreadable, or unparseable manifest produces no warning instead of a traceback, and it is skipped entirely under --plugin all, where there is no single plugin to check.
Privacy
Still no telemetry, no network calls, and nothing written to disk. The friction report reads two more local files than it did at v2.4.1 — ~/.claude/plugins/installed_plugins.json and known_marketplaces.json — plus the marketplace clone's own plugin.json, taking only version strings and the clone path. All three are state Claude Code already persists locally.
PRIVACY.md is updated to say so. Nothing leaves your machine.
Full changelog: v2.4.1...v2.5.0
v2.4.1
Production-target guard rails for Claude Code Bash commands.
Important
Two of these three fixes remove friction rather than add it — they turn a deny into a silent defer. That is the direction this project treats as needing justification, not the other way round, so each one below names the shell rule it rests on and the carve-outs that keep it from opening a hole. No target classification, verb table, or covered tool changed.
Highlights
A commit message that quotes an infrastructure command is no longer denied. The tokenizer rewrites newlines to ;, so every line of a heredoc body became its own simple command to inspect. Prose about infrastructure work quotes infrastructure commands, so a Conventional-Commit message describing a helm upgrade procedure got denied on its own body text — and the workaround was to write the message to a file first, turning every such commit into two steps. A python3 - <<'PY' script whose string literals mentioned kubectl apply hit the same wall.
A quoted delimiter (<<'EOF', <<"EOF", <<\EOF) makes the body literal text on stdin: the shell expands nothing and runs nothing from it. There is no shell behavior for the guard to miss, so scanning it can only ever produce false positives.
This is the one place the splitter removes segments instead of adding them, so it is deliberately narrow — only a quoted delimiter strips anything, only << outside quotes opens a heredoc, the closing delimiter must actually appear, and a body is never re-scanned for further openers. The shapes that merely look like heredocs cannot trigger it: <<< here-strings and $((1<<2)) left shifts carry no quoted delimiter. The reasoning is written up in design.md.
kubectl delete --help no longer denies a page that contacts nothing. Classification looked only at the verb, so asking for usage on a destructive subcommand was treated as the destructive subcommand. A help flag makes the CLI print usage and exit, so the segment now short-circuits before the evaluator runs on --help, -help, -h, or a leading help word.
Two carve-outs keep that from becoming a bypass:
sshhas no long options, sossh host --helpruns--helpon the remote and the connection to the target happens regardless.- On
docker/podman/nerdctl,-his--hostname, sodocker run -h box imagestays an ordinary mutating run.
The exemption is per simple command, so a help page chained ahead of a real mutating command still denies on the second segment.
The friction report no longer counts other guards' overrides as prod-guard's. OVERRIDE_SIG matched the bare phrase "override acknowledged", which every sibling guard emits. Under --plugin all — or --plugin foreground-guard — another guard's downgrades landed in prod-guard's counter and printed under a hardcoded PROD_GUARD_OVERRIDE label. That is a fabricated statistic with nothing in the output to signal it was wrong.
The signature is now anchored to prod-guard's own two reason shapes, and the line is dropped from the text report under --plugin all, where a single guard's count has no place in an all-guards header. The default --plugin prod-guard is unchanged.
Upgrading
No action required. Every change narrows when the guard fires; nothing you configured against v2.4.0 needs re-vetting. The one habit worth dropping is the heredoc workaround — commit messages that quote infrastructure commands no longer need to be written to a file first.
claude plugin marketplace update prod-guard
claude plugin update prod-guard@prod-guardRestart to apply. Both commands work headlessly and share Claude Desktop's plugin state, so they update a Desktop install too.
Everything since v2.4.0
Three fixes, closing two issues.
Fixes (3)
- fix(parsing): skip heredoc bodies opened with a quoted delimiter by @karlkfi in #32 (part 2 of #28)
- fix(help): defer when a segment only asks for usage text by @karlkfi in #31 (part 1 of #28)
- fix(friction-report): attribute override downgrades to prod-guard only by @karlkfi in #30 (closes #29)
Documentation
docs/design.mdgains Why a quoted heredoc body is the one place segments are removed — the four constraints above, and why this exception does not generalize.
Validation
389 tests pass, up from 363 at v2.4.0 — 26 new ones, the bulk of them fixtures for the two parsing changes. Stdlib unittest, no third-party dependencies, run on Python 3.10 through 3.13 in CI:
python3 -m unittest discover testsThe parsing cases matter more than the count here. Both fixes remove segments from inspection, so the tests that earn their keep are the ones asserting what still gets caught: a help page chained ahead of a mutating command, ssh host --help, docker run -h, an unterminated heredoc, an unquoted <<EOF body, and the here-string and left-shift shapes that must never strip anything. Guard decisions are exercised by invoking the script as a subprocess against a synthetic $HOME, never by running the guarded tool.
Full changelog: v2.4.0...v2.4.1
v2.4.0
v2.3.0
Minor release: target-repointing switch commands now deny outright instead of asking.
kubectl config use-context (and its alias config set current-context), kubectx, gcloud config set/unset and mutating gcloud config configurations, az account set, and docker context use previously asked, denying only on a prod-named target. They now deny regardless of the target: the switch repoints the shared ambient target of every parallel session even when aimed at a harmless dev context, and each tool has a per-command pin flag (--context/--project/--subscription) that the deny message names. PROD_GUARD_OVERRIDE still downgrades to a confirmation prompt for a genuinely intended switch. Credential logins, non-repointing config edits, and namespace switches (kubens, oc project) keep the ask.
Full Changelog: v2.2.0...v2.3.0
v2.2.0
Minor release: gh mutating-verb tiering so collaboration commands stop prod-classifying, plus target-precedence and gcloud read-only fixes.
- feat(gh): tier mutating verbs so collaboration metadata never prod-classifies by @karlkfi in #23
- fix: let config nonprod outrank the built-in prod heuristic by @karlkfi in #22
- fix(gcloud): classify
logging read/tailas read-only by @karlkfi in #20 - docs: document that third-party marketplaces don't auto-update by @karlkfi in #21
Housekeeping: backlog format migration (#15) and a test repair (#24).
Full Changelog: v2.1.0...v2.2.0
v2.1.0
Minor release: resolved targets now expand shell variables, sharpening classification accuracy, plus repository brand images.
- feat: expand shell variables in resolved targets (Q11) by @karlkfi in #14
- docs: add brand images (icon, favicon, social preview) by @karlkfi in #13
Full Changelog: v2.0.0...v2.1.0
v2.0.0
Major release: unpinned mutating commands now deny instead of prompting — a strict secure-by-default tightening of the ambient-context threat model.
A mutating infrastructure command that pins no explicit target (kubectl delete pod x with no --context, terraform apply with no TF_WORKSPACE, aws s3 rm … with no --profile, …) now denies with a self-healing fix-it naming the flag to add, instead of asking. The ambient target is clobber-prone shared state a parallel session can repoint between writing and running the command, so it can't run until the target is explicit. The deny is machine-actionable — the agent re-runs pinned (which then defers) in one round trip rather than stalling a human. Kube deny/ask reasons now also echo the parsed namespace alongside the context. PROD_GUARD_OVERRIDE still downgrades any deny → ask.
Full Changelog: v1.1.0...v2.0.0
v1.1.0
Minor release: broader tool coverage and sharper ambient-context resolution, so more real production targets are recognized before a mutating command runs.
New guarded tools
- Cover
pulumiandansible/ansible-playbookby @karlkfi in #4 - Cover
ssh <prod-host>with denylist-only deny by @karlkfi in #3
Sharper target resolution
- Classify kube-contexts by cluster server URL, not just name by @karlkfi in #2
- Classify the terraform backend state location, not just the workspace by @karlkfi in #1
- Classify and resolve AWS default and named
--profileaccounts for ambient targeting by @karlkfi in #6, #8 - Resolve eksctl's ambient AWS default profile by @karlkfi in #7
- Resolve pulumi's ambient selected stack for prod denial by @karlkfi in #9
Tooling
Full Changelog: v1.0.0...v1.1.0
v1.0.0
Initial release: a Claude Code PreToolUse hook that guards Bash infrastructure commands against two threats — prod blast-radius (mutating verb resolving to a production target → deny, PROD_GUARD_OVERRIDE=<reason> downgrades to a confirmation prompt) and ambient-context clobbering (mutating command relying on shared current-context/active-config state → ask, steering to per-command pinning).
- Covered tools: kubectl/oc, helm, flux, argocd, gcloud, aws/eksctl, az, terraform/tofu, docker/podman/nerdctl/docker-compose, gh, doctl, kubectx/kubens, kustomize
- Compound-command evaluation:
&&/|/;/newlines/$( )/backticks/sh -cbodies/wrappers are split and each segment judged - Fail-open on infrastructure errors, fail-closed on the security decision (unknown target + mutating verb always prompts); never emits
allow - Configurable prod/nonprod patterns:
~/.claude/prod-guard.json, project.claude/prod-guard.json,PROD_GUARD_*env vars — all additive over built-ins - 175 tests: decision matrix, adversarial bypass battery, per-tool verb sweeps, ambient-reader fixtures, robustness corpus
Install:
/plugin marketplace add karlkfi/claude-prod-guard
/plugin install prod-guard@prod-guard