import type { BashContext, Violation } from '../types'; import { BashRuleBase, EmptyRuleConfig } from '../rule-base'; import { FixHint } from '../fix-hint'; /** * Blocks `git commit` whose message is passed INLINE (`-m` / `--message` / `-am`) when that message * contains a backtick, a `$(`, or a newline — and hands back `git commit -F ` instead. * * ─── The incident: a commit message that HUNG for ten minutes, twice ─────────────────────────────── * An agent ran `git commit -q -m "…"` with a multi-paragraph message that happened to contain the * sentence: *"… `strings` on a .app built with --port 8084 contains no 8084."* The backticks are * COMMAND SUBSTITUTION. The shell ran `strings` with no arguments, `strings` read stdin, and stdin * never closed. The Bash tool SIGTERM'd it at its ten-minute cap. The retry kept the same sentence and * did it again — twenty minutes for one commit. * * ─── Why this is a GUARD and not a doc line ──────────────────────────────────────────────────────── * The agent then diagnosed it as "a guard caught a commit message that quoting a blocked command", * citing an unrelated deploy-guard note. Three facts settle that it was nothing of the kind, and they * are worth writing down because they are the reason the cure has to arrive BEFORE execution: * * - a PreToolUse guard denies INSTANTLY, before the command runs. This ran for ten minutes. * - `Exit code 143` is SIGTERM delivered to a RUNNING process, not a refusal. * - the first attempt was `git add -A && git commit -m "…"`, and afterwards the 45 files were * STAGED — so the command really executed, and then blocked while expanding its second word. * * A footnote in a doc could not have reached that agent at that moment; a refusal naming `-F` does. * * ─── The trigger is the METACHARACTER, never the words ───────────────────────────────────────────── * A message is PROSE, and prose goes through the shell's expansion rules whether or not it is meant * to. A blocklist of "dangerous commands" would have missed this entirely — `strings` is an ordinary * word, and the next hang will name an ordinary word too. So the rule matches backtick, `$(` and a * newline inside the message argument, and reads nothing at all into the surrounding sentence. * * ─── It fires on a SINGLE-QUOTED message too, and that is deliberate ─────────────────────────────── * `git commit -m 'a `backtick` here'` is, strictly, safe: single quotes suppress substitution, so the * shell would pass those backticks through literally. This guard blocks it anyway, and the false * positive is the point rather than an oversight — do not "fix" it. Two reasons: * * - single-quoting PROSE is fragile in the one way that matters. An English sentence eventually * contains an apostrophe ("doesn't", "the agent's"), which CLOSES the quote mid-message and drops * the rest of the sentence back into the shell's syntax — the exact state this guard exists to * prevent, reached by a message that looked safe when it was written. * - the cure is free. `-F` costs one Write tool call and works for every message, so being wrong * here spends seconds, while being right saves twenty minutes. A guard chooses its errors by what * each one COSTS, and these two costs are not close. * * `blocks a single-quoted message too` in the spec pins this, so a later reader meets the decision as * a failing test rather than as a bug report. * * ─── It matches the RAW command, and that is load-bearing ────────────────────────────────────────── * Every other bash guard here matches `ctx.commandCode`, which STRIPS heredoc bodies and quoted prose * precisely so a commit message merely MENTIONING `git push` is not read as a push. That stripping * deletes exactly the span this rule exists to inspect: on `commandCode` the message is already gone, * and the guard would be permanently blind. So it reads `ctx.command`. * * That is safe here because the rule is still blocklist-shaped and its cure can never itself be * blocked: `-F` writes the message to a file and passes a PATH, so nothing about the message text can * make the replacement command match this guard. There is no input for which an agent is left with no * accepted spelling. */ export declare class CommitMessageSubstitutionGuardRule extends BashRuleBase { constructor(); private readonly scan; readonly description: string; get fixHint(): FixHint; check(ctx: BashContext): readonly Violation[]; private allow; private block; private message; private what; private truncate; private logDecision; }