git-guard: join backslash line-continuations before subcommand parsing (push rail slips on wrapped 'git \ push') #50

Closed
opened 2026-07-23 14:55:14 +00:00 by crenshawdev · 0 comments
crenshawdev commented 2026-07-23 14:55:14 +00:00 (Migrated from github.com)

Severity: low · Area: git-guard · surfaced in the post-v1.2.0 cross-model review sweep. Framed as a consistency hardening, not a design change: a sibling seam already does the join this one omits.

What happens

The push rail (git pushask) can be slipped by writing the command with a backslash line-continuation, so an agent wrapping a long command across lines pushes without the rail firing.

Mechanism (verified against source)

cadence-core/bin/git-guard.mjs:75: for (const segment of stripped.split(/&&|\|\||[;|\n]/)) treats a bare \n as a command separator, and nothing first joins backslash-continued lines. So:

git \
  push origin main

splits into segment 1 = git \ (first non-option word is \, so the subcommand resolves to \) and segment 2 = push origin main (no git word, skipped). isPush is false, the guard returns silently, and the push is allowed with no prompt.

Why this is a hardening, not a design question

git-guard documents "conservative silence" for unrecognized shapes by design, and truly exotic evasions (e.g. backtick command substitution) are reasonably out of scope. But the backslash-continuation case is different: self-verify.mjs:187 already normalizes it — const joined = text.replace(/\\\n\s*/g, ' '); — before its own command parsing. git-guard should do the same, so the two seams parse commands consistently and the push rail sees a wrapped git push as the push it is.

Fix direction

Join backslash line-continuations before splitting in gitSubcommands (mirror self-verify.mjs:187: collapse \\\n\s* to a space). Scope is the backslash case only; other exotic shell shapes remain intentionally conservative-silent per the file's stated design.

**Severity:** low · **Area:** git-guard · surfaced in the post-v1.2.0 cross-model review sweep. Framed as a **consistency hardening**, not a design change: a sibling seam already does the join this one omits. ## What happens The push rail (`git push` → `ask`) can be slipped by writing the command with a backslash line-continuation, so an agent wrapping a long command across lines pushes without the rail firing. ## Mechanism (verified against source) `cadence-core/bin/git-guard.mjs:75`: `for (const segment of stripped.split(/&&|\|\||[;|\n]/))` treats a bare `\n` as a command separator, and nothing first joins backslash-continued lines. So: ``` git \ push origin main ``` splits into segment 1 = `git \` (first non-option word is `\`, so the subcommand resolves to `\`) and segment 2 = `push origin main` (no `git` word, skipped). `isPush` is false, the guard returns silently, and the push is allowed with no prompt. ## Why this is a hardening, not a design question `git-guard` documents "conservative silence" for unrecognized shapes by design, and truly exotic evasions (e.g. backtick command substitution) are reasonably out of scope. But the backslash-continuation case is different: `self-verify.mjs:187` **already** normalizes it — `const joined = text.replace(/\\\n\s*/g, ' ');` — before its own command parsing. `git-guard` should do the same, so the two seams parse commands consistently and the push rail sees a wrapped `git push` as the push it is. ## Fix direction Join backslash line-continuations before splitting in `gitSubcommands` (mirror `self-verify.mjs:187`: collapse `\\\n\s*` to a space). Scope is the backslash case only; other exotic shell shapes remain intentionally conservative-silent per the file's stated design.
Sign in to join this conversation.
No description provided.