Lines 1-41javascript
2// rules/check-git-discard.mjs — blocks a git command that would silently
3// destroy uncommitted work.
5// `git checkout -- <paths>` and `git restore <paths>` overwrite the working
6// tree with no undo. In an agent session, reverting a dirty file is almost
7// never wanted without a stash (origin: 2026-06-10, a stray `git checkout --`
8// during cache debugging wiped a file's entire uncommitted rewrite). This is a
9// PreToolUse hook on Bash — it must fire BEFORE the command runs; a post-hoc
10// check can't un-destroy work — so it is wired in .claude/settings.json, not
13// Scope is deliberately narrow (prefer false negatives over blocking real
14// work): only the `--` pathspec form of checkout (branch switching passes) and
15// `git restore` forms that touch the working tree (`--staged`-only passes).
16// A literal `# discard-ok` comment in the command lets a deliberate discard
19// Usage: hook mode (default): reads PreToolUse JSON on stdin (exit 2 blocks)
20// node rules/check-git-discard.mjs --test (self-test)
22import { execFileSync } from 'node:child_process';
23import { readFileSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs';
HighChild Process
Package source references child process execution.
rules/git-hygiene/no-git-discard.mjsView on unpkg · L21 24import { tmpdir } from 'node:os';
25import { join } from 'node:path';
27// ── pure ─────────────────────────────────────────────────────────────────────
29const OPERATORS = new Set([';', '&&', '||', '|', '&', '|&']);
31// Shell-ish tokeniser: respects quotes (so a commit message mentioning
32// "git checkout --" doesn't false-positive) and emits shell operators as
33// their own tokens. Returns null on unparseable input (unclosed quote) —
34// callers must be permissive then.
35export function tokenize(cmd) {
40 const push = () => { if (started) tokens.push(cur); cur = ''; started = false; };
41 while (i < cmd.length) {