diff --git a/corpus/CLAUDE.learned.md b/corpus/CLAUDE.learned.md index c2753870..29611a5a 100644 --- a/corpus/CLAUDE.learned.md +++ b/corpus/CLAUDE.learned.md @@ -42,3 +42,4 @@ Engine-only install drops this file; reflect Accepted global rules land here. - When handing off a blocked action for me to run myself, always write it to a small script file and hand back exactly one `! bash ` / `! python3 ` line — never paste inline multi-line, multi-flag, or `&&`/`&`-backgrounded shell text into chat for me to copy. This applies on the very first handoff and every one after it, not after I complain about copy-paste pain. Pasted shell breaks in transit — wrapped lines, lost quoting, an `&` that detaches a job — while one `!` line runs exactly the file that was written. - Never pass a quoted command string through `sudo -i`, `su -`, or `su -l`. Those start a login shell that reads the command a second time, after the first shell already used up the quotes, so the string splits on spaces and its first word runs alone (`sudo -u demo -H -i bash -lc 'set -u; echo one'` fails with `set: -c: invalid option`; the same line without `-i` prints `one`). Copy the script to the host and run it by path, or drop `-i`. - The shell behind the Bash tool may be zsh, which does not split an unquoted variable into words: `for x in $LIST` runs once with the whole list as one item, exits 0, and reads as "all done". Run any loop over a list under bash (`bash <<'EOF' ... EOF`), count lookups that failed as unchecked rather than done, and spot-check one item before acting on the loop's summary. This is zsh's `SH_WORD_SPLIT` option being off by default (zsh manual, Options, https://zsh.sourceforge.io/Doc/Release/Options.html#index-SH_005fWORD_005fSPLIT). +- Before asking me whether to run a script, read its whole body, not its header comment, usage text, or `--dry-run` output, and name in the question every process it kills, file it deletes, and service it restarts. A header states what the author meant; the body is what runs, so a consent question built from the header asks me to approve something other than what will happen. No known prior art. diff --git a/tests/test_read_script_before_consent_rule.py b/tests/test_read_script_before_consent_rule.py new file mode 100644 index 00000000..c4b3ebe6 --- /dev/null +++ b/tests/test_read_script_before_consent_rule.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Pin the learned rule: read a script's whole body before asking to run it. + +A detector cannot judge this reliably, so the test proves the rule is present +with its trigger, required action, and reason; deleting or hollowing it fails here. +""" +import os +import unittest + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LEARNED = os.path.join(REPO_ROOT, "corpus", "CLAUDE.learned.md") + + +def working_style_section(): + with open(LEARNED, encoding="utf-8") as handle: + text = handle.read() + start = text.index("# Working style") + end = text.find("\n# ", start + 1) + return text[start:] if end == -1 else text[start:end] + + +def bullet_starting(prefix): + for line in working_style_section().splitlines(): + if line.startswith(prefix): + return line + raise AssertionError(f"no working-style rule starts with {prefix!r}") + + +class TestReadScriptBeforeConsent(unittest.TestCase): + PREFIX = "- Before asking me whether to run a script" + + def test_rule_requires_the_whole_body_not_the_header(self): + rule = bullet_starting(self.PREFIX) + self.assertIn("read its whole body", rule) + self.assertIn("header comment", rule) + self.assertIn("`--dry-run` output", rule) + + def test_rule_names_what_the_question_must_list(self): + rule = bullet_starting(self.PREFIX) + for effect in ("process it kills", "file it deletes", "service it restarts"): + self.assertIn(effect, rule) + + +if __name__ == "__main__": + unittest.main()