Skip to content

Em Dash Guard Boundary โ€‹

What is this? โ€‹

A same-day correction to a wrong claim in .agents/rules/project/key-conventions.md about what guard-em-dash.sh actually covers. โ€‹

  • The rule said the hook covers only Write and Edit, and never sees a file written through a shell heredoc. That is false: the .claude/settings.json matcher is Write|Edit|Bash, and the Bash branch scans any command whose text CONTAINS git or gh (see the correction below: that is a substring glob, not a check on which program runs). A heredoc feeding a git commit message or a gh PR body IS caught, which is exactly why a dispatched agent got blocked twice today on that pattern, before ever touching a file. Three sessions read the wrong sentence, treated heredoc writes as fully unguarded, and a compensating control got built on that false premise.

What is the current state? โ€‹

Fixed, tested, committed, and pushed. Commit cf910262 on feat/admin-and-merchant-portals. โ€‹

  • Regenerated the four synced copies via npm run sync:agents (AGENTS.md, .cursor/rules/key-conventions.mdc, .github/instructions/key-conventions.instructions.md). GEMINI.md needed no change since it only imports AGENTS.md.
  • npm run sync:agents -- --check confirmed no residual drift after the regeneration.
  • Verified the em dash count stayed at 0 (baseline) on every file touched, before and after.

CORRECTION, 2026-08-30: the gate is a SUBSTRING GLOB, not a check on which program runs. โ€‹

  • The line below said the Bash branch fires "only on a command whose text names git or gh". The guard's actual matcher is case "$cmd" in *git\ *|*gh\ *|git\ *|gh\ *), four globs over the whole command string. Any command containing gh or git ANYWHERE satisfies it, which ordinary English does constantly.
  • Verified against the live hook on 2026-08-30, same technique as the original ten payloads: echo "read it through <dash> carefully" BLOCKED, high BLOCKED, enough BLOCKED, digit BLOCKED (that one is the git glob). A plain echo with none of those substrings is still ALLOWED, and light<dash>house is ALLOWED because the gh there is not followed by a space.
  • Nothing needs loosening. The error runs in the safe direction: the guard scans MORE commands than the docs claimed, never fewer, so no em dash slips through a gap this misdescription hid. What it changes is the reader's model, which is why it is worth correcting rather than shrugging at.
  • Why the original pass missed it: the ten payloads all tested the content half of the boundary (a dash arriving by variable, by substitution, by git checkout) against commands that really were git commands. None tested a NON-git command that happens to contain the substring, so the glob never got to disagree with the prose.

What the boundary actually is, established by testing before writing anything. โ€‹

  • The Bash branch of guard-em-dash.sh fires on a command whose text contains git or gh . Anything else (a plain echo, cat, or heredoc with neither substring) is never scanned, regardless of what it contains.
  • Even a matching command is only scanned as literal text. It cannot see content that arrives without being spelled out there.
  • The corrected sentence now in key-conventions.md: "The hook also scans Bash commands, so a heredoc feeding a commit message or PR body is caught before it lands. Its gate is a SUBSTRING glob over the whole command text (git or gh occurring anywhere in it), NOT a check that the command runs git or gh: through, high, enough and digit all satisfy it and all get scanned, verified against the live hook on 2026-08-30. That errs safe, scanning more commands than it needs to rather than fewer, so nothing wants loosening. What it cannot do is see past the literal text, so an em dash reaches disk unscanned whenever it arrives some other way: a heredoc to a plain file, a shell variable, a copied or moved file, a program's own output, or content a git checkout/git apply restores. CI catches that path at PR time."

Tested vs. reasoned, named explicitly. โ€‹

  • Tested, by piping ten crafted JSON payloads directly into the live .claude/hooks/guard-em-dash.sh script (the same technique its own committed test suite uses) and reading the real exit code:
    • Literal dash in a git commit command text: BLOCKED (control, matches the committed test suite).
    • Literal dash in a non-git echo: ALLOWED (control, matches the committed test suite's leaves non-git commands alone case). Only because that particular echo carried neither substring, see the 2026-08-30 correction above.
    • A git log whose format string is built by command substitution ($(python3 -c ...)), dash never literal in the command text: ALLOWED.
    • A git commit -m "$VAR" referencing a shell variable, dash never literal in the command text: ALLOWED.
    • git checkout HEAD -- file, restoring tracked content that has a dash, no dash in the command text itself: ALLOWED.
    • git apply, restoring patched content: ALLOWED.
    • cp and mv of a file whose content has a dash, no git/gh in the command at all: ALLOWED (both).
    • A program's own output redirected to a file (python3 -c "print(chr(8212))" > file), no git/gh in the command: ALLOWED.
    • A git commit whose message is generated by a subprocess via command substitution: ALLOWED.
  • Reasoned, not tested: whether a literal em dash inside a non-git heredoc (e.g. cat > file.txt <<EOF with the character typed directly in the body) would be scanned. Could not test this directly without typing a literal em dash into a command, which this task's own hard constraint forbade. Confirmed instead by reading the guard's source (the git/gh substring gate does not distinguish a heredoc body from any other command shape) and by the committed test suite's echo ${DASH} case, which exercises the identical code path.
  • Attempted and deliberately abandoned: a live end-to-end proof in an isolated scratch git repo (write a file with a real dash outside git, commit it, then git checkout an older revision to restore it, and grep the result). The guard-pm-lane.sh hook refused the command because it flagged a bare file.txt write-target token, even though the whole thing lived under /tmp/.../scratchpad/. This session's CLAUDE_ALLOW_PM_BUILD=1 authorization was scoped explicitly to key-conventions.md and the sync output, nothing else, so the override was not used to push past it. The ten direct hook-script invocations above already exercise the real guard code path and were treated as sufficient without it.

A second, unrelated guard also fired, and is worth recording. โ€‹

  • The first commit attempt used the standard git commit -m "$(cat <<'EOF' ... EOF)" heredoc pattern. guard-backtick-body.sh blocked it: live $( ) substitution inside a double-quoted -m argument is treated as a shell-injection shape (a real incident on 2026-08-13 executed a git push this way), regardless of how clean the content inside is. Its own message names the fix: write the message to a file with a quoted heredoc, then use -F. Re-ran that way; the pre-verified, em-dash-checked message file was used directly with git commit -F.
  • Also found mid-task: another session is concurrently modifying .github/workflows/ai-changelog.yml and .github/workflows/discord-notify.yml in this same working directory (the main checkout, not a dedicated worktree). git add was scoped to the four exact target files throughout, never -A or ., and staging was verified against git diff --cached --name-only before every commit.
  • #945: A text-scanning guard cannot tell content from a pattern that matches content: same root cause (a guard that scans raw command text) in the opposite failure direction. #945 is a false positive (the guard blocks a grep pattern that searches FOR the character); this project's finding is a false negative (the guard misses a character that never appears literally in the command). #945's own caution against changing the guard's behavior without the operator's word is why this project only fixed the documentation, not guard-em-dash.sh itself.
  • docs/projects/pm-lane-guard/: the sibling guard whose door (CLAUDE_ALLOW_PM_BUILD=1) this project used, and whose own header comment already names #945 as a design consideration.
  • tooling/scripts/__tests__/guard-em-dash.test.js: the committed test suite that independently corroborates the git/gh substring gate.

Built with VitePress