From c184fd55ead326b7b6cf371e988dc33b5fef60ba Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Fri, 24 Jul 2026 21:12:53 -0700 Subject: [PATCH] guardrails: silence the intentional SC2016 in prod-guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit shell-lint flagged the literal `$(` / backtick patterns the read-only classifier searches for inside a command string. Expanding them is exactly what must not happen — the check exists to refuse commands containing command substitution, so the single quotes are the point. Same treatment and rationale as the existing disables in render-secrets.sh and verify-centralis-render.sh. Verified with the pinned shellcheck v0.11.0 (sha256 matching shell-lint.yml): all 34 scripts in the tree clean. Hook behaviour unchanged — the classifier test matrix still allows reads and asks on mutations and compound smuggling. Also documents that ~/.claude/hooks/ carries a mirror of prod-guard.sh so sessions started outside this repo are covered, and that the blanket ssh allow rules were removed from global settings so the hook cannot be out-ranked. --- .claude/hooks/README.md | 25 ++++++++++++++++++++++--- .claude/hooks/prod-guard.sh | 3 +++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.claude/hooks/README.md b/.claude/hooks/README.md index 672654c..0075e86 100644 --- a/.claude/hooks/README.md +++ b/.claude/hooks/README.md @@ -54,12 +54,31 @@ fails toward the prompt rather than toward silent execution. ## lint-after-edit needs shellcheck on PATH -It exits quietly when shellcheck is absent, so it is **inert until installed** — a -missing local tool must not break editing, and `shell-lint` CI is still the backstop. -Install the same pinned version CI uses: +It exits quietly when shellcheck is absent — a missing local tool must not break +editing, and `shell-lint` CI is still the backstop. v0.11.0 (the version CI pins) is +installed at `~/.local/bin/shellcheck`; if you move machines, reinstall it: ```bash VER=v0.11.0 curl -fsSL "https://github.com/koalaman/shellcheck/releases/download/${VER}/shellcheck-${VER}.linux.x86_64.tar.xz" \ | tar -xJ --strip-components=1 -C ~/.local/bin "shellcheck-${VER}/shellcheck" ``` + +Keep it matched to `shell-lint.yml`'s pin. A drifted shellcheck grows *new* warnings +and fails CI on an unrelated push, which is why that workflow pins the version and +its sha256 rather than using `apt-get install`. + +## The global copy + +`~/.claude/settings.json` runs `prod-guard.sh` from `~/.claude/hooks/` as well, so a +session started **outside** this repo is covered too — otherwise the guard would be +trivially bypassed by working from `~/Code`. That copy is a mirror; this one is +canonical. If you change the classifier here, copy it over: + +```bash +cp .claude/hooks/prod-guard.sh ~/.claude/hooks/prod-guard.sh +``` + +The blanket `Bash(ssh prod:*)` / `Bash(ssh beta:*)` allow rules were removed from +global settings at the same time, so the hook is the only thing granting live-host +access. If it is missing or broken, nothing allows the command and you get a prompt. diff --git a/.claude/hooks/prod-guard.sh b/.claude/hooks/prod-guard.sh index 3a6a05b..5d81dda 100755 --- a/.claude/hooks/prod-guard.sh +++ b/.claude/hooks/prod-guard.sh @@ -55,6 +55,9 @@ is_readonly() { case "$stripped" in *'>'*) return 1 ;; esac # Command substitution can hide anything; refuse to reason about it. + # shellcheck disable=SC2016 # single quotes are the point: these are literal + # `$(` and backtick characters being searched for inside the command text, not + # expansions to perform. Expanding them here would defeat the check entirely. case "$cmd" in *'$('*|*'`'*) return 1 ;; esac while IFS= read -r seg; do