From e496f234b99d06817b924094650b65a746dbcca7 Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 24 Oct 2026 20:36:53 +0000 Subject: [PATCH] fix(careful): scan the command as one string, so a newline still separates commands from the message (#1060) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- careful/bin/mask-commit-message.awk | 28 ++++++++++++++++------------ test/hook-scripts.test.ts | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/careful/bin/mask-commit-message.awk b/careful/bin/mask-commit-message.awk index 992a76de0..b827b66f0 100644 --- a/careful/bin/mask-commit-message.awk +++ b/careful/bin/mask-commit-message.awk @@ -1,6 +1,8 @@ # mask-commit-message.awk — read by careful/bin/check-careful.sh (#1060). # Prints the command with the quoted message argument of git commit -# (-m, -am, --message) replaced by an empty quoted string. +# (-m, -am, --message) replaced by an empty quoted string. The command is +# scanned as one string: newlines are kept, since bash runs each line as its +# own command, and quote state carries across lines. function handle_msg(s, n, i, j, safe) { if (i > n) return i if (substr(s, i, 1) == "'") { @@ -24,34 +26,36 @@ function handle_msg(s, n, i, j, safe) { } return i } -{ - n = length($0); sq = 0; dq = 0; bs = 0; i = 1 +{ cmd = (NR > 1) ? cmd "\n" $0 : $0 } +END { + s = cmd + n = length(s); sq = 0; dq = 0; bs = 0; i = 1 while (i <= n) { - c = substr($0, i, 1) + c = substr(s, i, 1) if (bs) { result = result c; bs = 0; i++; continue } if (!sq && dq && c == "\\") { result = result c; bs = 1; i++; continue } if (!dq && c == "'") { sq = !sq; result = result c; i++; continue } if (!sq && c == "\"") { dq = !dq; result = result c; i++; continue } if (sq || dq) { result = result c; i++; continue } - rest = substr($0, i) + rest = substr(s, i) if (rest ~ /^--message=/) { result = result "--message="; i += 10 - i = handle_msg($0, n, i); continue + i = handle_msg(s, n, i); continue } if (rest ~ /^--message([ \t]|$)/) { result = result "--message"; i += 9 - while (i <= n && substr($0,i,1) ~ /[ \t]/) { result = result substr($0,i,1); i++ } - i = handle_msg($0, n, i); continue + while (i <= n && substr(s,i,1) ~ /[ \t]/) { result = result substr(s,i,1); i++ } + i = handle_msg(s, n, i); continue } if (rest ~ /^-am([ \t]|$)/) { result = result "-am"; i += 3 - while (i <= n && substr($0,i,1) ~ /[ \t]/) { result = result substr($0,i,1); i++ } - i = handle_msg($0, n, i); continue + while (i <= n && substr(s,i,1) ~ /[ \t]/) { result = result substr(s,i,1); i++ } + i = handle_msg(s, n, i); continue } if (rest ~ /^-m([ \t]|$)/) { result = result "-m"; i += 2 - while (i <= n && substr($0,i,1) ~ /[ \t]/) { result = result substr($0,i,1); i++ } - i = handle_msg($0, n, i); continue + while (i <= n && substr(s,i,1) ~ /[ \t]/) { result = result substr(s,i,1); i++ } + i = handle_msg(s, n, i); continue } result = result c; i++ } diff --git a/test/hook-scripts.test.ts b/test/hook-scripts.test.ts index 7df5262c2..8fb85d75a 100644 --- a/test/hook-scripts.test.ts +++ b/test/hook-scripts.test.ts @@ -434,6 +434,26 @@ describe('check-careful.sh', () => { expect(fs.existsSync(path.join(path.dirname(CAREFUL_SCRIPT), 'mask-commit-message.awk'))).toBe(true); }); + // Newlines separate commands, and quote state carries across them. The + // per-line scanner joined lines (`-m xtruncate`) and restarted quote + // state on every line, so a quoted line break could hide a real command. + test.each([ + ['git commit -m x\ntruncate -s 0 f', 'SQL TRUNCATE'], + ['git commit -m wip\nrm -rf build', 'recursive delete'], + ["git commit -m \"a\n-m 'x\" && rm -rf build && echo '", 'recursive delete'], + ])('a command on the next line still asks: %j', (command, reason) => { + const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput(command)); + expect(exitCode).toBe(0); + expect(output.hookSpecificOutput?.permissionDecision).toBe('ask'); + expect(output.hookSpecificOutput?.permissionDecisionReason).toContain(reason); + }); + + test('a multi-line quoted message is masked as one message', () => { + const { exitCode, output } = runHook(CAREFUL_SCRIPT, carefulInput('git commit -m "fix: guard migrations\n\nNo more drop table in the seed script."')); + expect(exitCode).toBe(0); + expect(output.hookSpecificOutput?.permissionDecision).toBeUndefined(); + }); + test('a partial install without the masking program still asks on the message text', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-careful-nomask-')); try {