claude-dev-env 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CLAUDE.md +20 -57
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
  3. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  12. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  13. package/agents/CLAUDE.md +1 -1
  14. package/agents/code-verifier.md +36 -7
  15. package/bin/codex-compat.mjs +104 -0
  16. package/bin/codex-compat.test.mjs +51 -0
  17. package/codex-capability-map.json +13 -0
  18. package/docs/CODE_RULES.md +2 -0
  19. package/docs/codex-compatibility.md +25 -0
  20. package/docs/nas-ssh-invocation.md +96 -12
  21. package/docs/references/code-review-enforcement.md +31 -6
  22. package/hooks/blocking/CLAUDE.md +3 -0
  23. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
  24. package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
  25. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  26. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  27. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  28. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  29. package/hooks/blocking/state_description_blocker.py +1 -1
  30. package/hooks/blocking/stop_dispatcher.py +1 -1
  31. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  32. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  33. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  34. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  35. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  36. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
  37. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  38. package/hooks/git-hooks/CLAUDE.md +3 -0
  39. package/hooks/git-hooks/conftest.py +30 -0
  40. package/hooks/git-hooks/gate_utils.py +2 -2
  41. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  42. package/hooks/git-hooks/pre_push.py +75 -4
  43. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  44. package/hooks/git-hooks/test_config.py +0 -15
  45. package/hooks/git-hooks/test_gate_utils.py +3 -15
  46. package/hooks/git-hooks/test_pre_commit.py +1 -15
  47. package/hooks/git-hooks/test_pre_push.py +236 -27
  48. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  49. package/hooks/hooks.json +0 -12
  50. package/hooks/hooks_constants/CLAUDE.md +5 -1
  51. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  52. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  53. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  54. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  55. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  56. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  57. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  58. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  59. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  60. package/package.json +4 -2
  61. package/rules/CLAUDE.md +17 -23
  62. package/rules/agent-spawn-protocol.md +6 -6
  63. package/rules/anti-corollary-tests.md +1 -1
  64. package/rules/bdd.md +1 -1
  65. package/rules/cleanup-temp-files.md +10 -4
  66. package/rules/code-standards.md +7 -0
  67. package/rules/conservative-action.md +1 -5
  68. package/rules/context7.md +0 -4
  69. package/rules/destructive-commands.md +47 -0
  70. package/rules/doc-inventory-integrity.md +48 -0
  71. package/rules/doc-prose-cuts.md +58 -0
  72. package/rules/docstring-prose-matches-implementation.md +10 -2
  73. package/rules/durable-post-artifacts.md +0 -4
  74. package/rules/eli11-replies.md +31 -0
  75. package/rules/explore-thoroughly.md +4 -4
  76. package/rules/falsify-before-green.md +68 -0
  77. package/rules/file-global-constants.md +1 -1
  78. package/rules/filesystem-search.md +51 -0
  79. package/rules/gh-cli-conventions.md +27 -0
  80. package/rules/git-workflow.md +26 -0
  81. package/rules/hedging-claims.md +9 -0
  82. package/rules/long-horizon-autonomy.md +0 -4
  83. package/rules/measurement-denominators.md +48 -0
  84. package/rules/nas-ssh-invocation.md +23 -5
  85. package/rules/parallel-tools.md +2 -2
  86. package/rules/plain-illustrative-docstrings.md +3 -7
  87. package/rules/plain-language.md +2 -0
  88. package/rules/proof-of-work-pr-comments.md +0 -4
  89. package/rules/re-stage-before-commit.md +2 -0
  90. package/rules/research-mode.md +10 -0
  91. package/rules/shell-invocation.md +21 -0
  92. package/rules/testing.md +4 -0
  93. package/rules/verified-commit-gate-skip.md +3 -27
  94. package/rules/verify-before-asking.md +5 -0
  95. package/rules/windows-filesystem-safe.md +1 -1
  96. package/rules/workers-done-before-complete.md +4 -0
  97. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  98. package/scripts/codex_capability_bridge.py +171 -0
  99. package/scripts/codex_compat_materializer.py +1087 -0
  100. package/scripts/codex_compat_watcher.py +502 -0
  101. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  102. package/scripts/invoke_code_review.py +11 -4
  103. package/scripts/sync_to_cursor/rules.py +0 -10
  104. package/scripts/test_invoke_code_review.py +143 -0
  105. package/scripts/test_invoke_code_review_chain.py +1 -1
  106. package/scripts/test_invoke_code_review_contract.py +1 -1
  107. package/scripts/tests/test_code_review_constants.py +80 -0
  108. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  109. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  110. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  111. package/scripts/tests/test_sync_to_cursor.py +0 -1
  112. package/skills/autoconverge/workflow/converge.mjs +1 -1
  113. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  114. package/skills/condensing-instructions/SKILL.md +42 -51
  115. package/skills/fresh-branch/CLAUDE.md +1 -1
  116. package/skills/fresh-branch/SKILL.md +5 -6
  117. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  118. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  119. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  120. package/skills/orchestrator/SKILL.md +23 -9
  121. package/skills/orchestrator-refresh/SKILL.md +20 -1
  122. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  123. package/skills/session-log/SKILL.md +1 -1
  124. package/rules/claude-md-orphan-file.md +0 -28
  125. package/rules/cleanup-command-forms.md +0 -23
  126. package/rules/code-reviews.md +0 -11
  127. package/rules/env-var-table-code-drift.md +0 -10
  128. package/rules/gh-body-file.md +0 -5
  129. package/rules/gh-paginate.md +0 -3
  130. package/rules/hook-prose-matches-detector.md +0 -15
  131. package/rules/no-historical-clutter.md +0 -26
  132. package/rules/no-inline-destructive-literals.md +0 -9
  133. package/rules/no-justification-noise.md +0 -61
  134. package/rules/package-inventory-stale-entry.md +0 -25
  135. package/rules/right-sized-engineering.md +0 -28
  136. package/rules/self-contained-docs.md +0 -17
  137. package/rules/shell-invocation-policy.md +0 -5
  138. package/rules/state-what-is.md +0 -25
  139. package/rules/tdd.md +0 -7
@@ -0,0 +1,13 @@
1
+ {
2
+ "schema_version": 1,
3
+ "capabilities": [
4
+ {"source": "TaskList", "aliases": ["task_list", "tasks", "Task.list"], "target": "update_plan", "transformation": "snapshot", "status": "fallback", "confidence": "medium", "manual_review": true, "required_fields": {"plan": "plan"}},
5
+ {"source": "TaskCreate", "aliases": ["task_create", "tasks.create", "create_task"], "target": "update_plan", "transformation": "append/replace-plan-item", "status": "mapped", "confidence": "high", "manual_review": false, "required_fields": {"name": "string", "status": "status"}},
6
+ {"source": "TaskUpdate", "aliases": ["task_update", "tasks.update", "update_task"], "target": "update_plan", "transformation": "status mutation", "status": "mapped", "confidence": "high", "manual_review": false, "required_fields": {"name": "string", "status": "status"}},
7
+ {"source": "Task", "aliases": ["Agent", "task", "agent", "spawn_agent", "spawn", "spawnAgent"], "target": "multi_agent_v1__spawn_agent", "transformation": "spawn request", "status": "mapped", "confidence": "medium", "manual_review": true, "required_fields": {"prompt": "string"}},
8
+ {"source": "SendMessage", "aliases": ["send_message", "agent.send", "send_input", "message", "sendMessage"], "target": "multi_agent_v1__send_input", "transformation": "agent input", "status": "mapped", "confidence": "high", "manual_review": false, "required_fields": {"message": "string"}},
9
+ {"source": "TaskOutput", "aliases": ["task_output", "wait", "TaskWait"], "target": "multi_agent_v1__wait_agent", "transformation": "wait request", "status": "mapped", "confidence": "high", "manual_review": false, "required_fields": {"task_id": "string"}},
10
+ {"source": "TaskStop", "aliases": ["task_stop", "stop_task", "close_agent", "stop", "stopAgent"], "target": "multi_agent_v1__close_agent", "transformation": "close request", "status": "mapped", "confidence": "high", "manual_review": false, "required_fields": {"task_id": "string"}},
11
+ {"source": "ScheduleWakeup", "aliases": ["schedule_wakeup", "wake_up", "cron_wakeup"], "target": null, "transformation": "explicit unsupported result; future automation surface requires confirmation", "status": "unsupported", "confidence": "high", "manual_review": true}
12
+ ]
13
+ }
@@ -60,6 +60,8 @@ Advisory only, never blocking: soft advisory at >= 400 lines, strong nudge at >=
60
60
  Never: ABC for single impl, DI frameworks, factory for single type. Always: functions when no state, concrete classes, simple imports.
61
61
  Parameters follow YAGNI: add an optional parameter when a caller varies the value; when every call site passes the same value, make it required or inline the constant. Remove parameters no caller passes and no body reads.
62
62
 
63
+ **Complexity budget — state it BEFORE implementation:** files (target 1-2, max 3), lines (~50-300). Checkpoints: Is this the MINIMUM? Can it be fewer files? Functions instead of classes?
64
+
63
65
  ## 7.5 SOLID PRINCIPLES
64
66
 
65
67
  **SRP always applies** — one reason to change per function/class/module. **OCP, LSP, ISP, DIP apply only where two or more concrete implementations already share a contract**; with a single concretion §7 wins (concrete classes, direct imports, YAGNI — introduce the abstraction at the commit that adds the second concretion). Misapplication signals: interface/ABC with exactly one implementation, SRP-splitting a cohesive class by size alone, abstract factories for one product, DI containers where every injected type has one concretion.
@@ -0,0 +1,25 @@
1
+ # Codex compatibility entry point
2
+
3
+ `codex-compat` is an optional, explicit bridge from this package's Claude-oriented source tree to Codex-compatible records. The existing `claude-dev-env` installer is unchanged and does not invoke it.
4
+
5
+ ## Materialization
6
+
7
+ Run `codex-compat materialize --source-root <claude-root> --target-root <codex-root>`. The command defaults to a dry run; add `--apply` to publish files. Use `--python <command>` or `CODEX_COMPAT_PYTHON` to select Python. If no usable interpreter is found, the command reports that condition. The launcher passes an argv array, never a shell command.
8
+
9
+ The Python materializer maps Claude `_shared/`, `agents/`, `hooks/`, `rules/`, and `scripts/` into the target according to the package's compatibility materialization rules. Claude agent frontmatter is converted to Codex TOML metadata. Unsupported Claude metadata is reported, rather than silently treated as equivalent.
10
+
11
+ Rules, hooks, and scripts that have no safe Codex runtime equivalent remain inert or source-only. They are preserved for inspection and are not executed as translated target tools. The capability bridge likewise emits declarative records only; it never invokes the translated surface.
12
+
13
+ Materialization uses a compatibility manifest to identify generated files. Dry runs report the plan without writing. Apply mode uses safe link/copy fallback where linking is unavailable, writes atomically, removes only stale managed files, and rolls back managed changes on failure. A failed rollback reports that reconciliation is required.
14
+
15
+ The manifest hash decides who owns a target file. A file whose hash still matches the manifest is one the tool wrote, so a later run refreshes it in place. A file whose hash differs is one you edited, so the run preserves it and reports a conflict. A file the manifest does not record at all is adopted only when its bytes already match the plan, which is what an interrupted run leaves behind; any other unrecorded file is preserved, and the error names the file to move or delete.
16
+
17
+ A missing or unreadable source root is an error, and the run changes nothing. An existing source root holding no agents makes every managed file stale, so the run refuses to delete them and exits non-zero; add `--allow-prune-all` to remove them on purpose.
18
+
19
+ ## Capability bridge
20
+
21
+ Run `codex-compat bridge --surface <name> --payload '<json-object>'`. The bridge exposes the Python translation logic directly. `TaskCreate` and `TaskUpdate` map to `update_plan`; spawn, message, wait, and stop map to multi-agent surfaces. `ScheduleWakeup` is explicitly unsupported and requires manual review.
22
+
23
+ ## Roots and safety
24
+
25
+ Both roots are caller-supplied. The tool never writes to `.agents` or `CODEX_HOME` automatically; pass those locations explicitly when desired. No personal paths or secrets are embedded in the package.
@@ -1,23 +1,107 @@
1
- # NAS SSH Invocation Policy
1
+ # Running Commands on the NAS
2
2
 
3
- Full detail behind the always-on `rules/nas-ssh-invocation.md` kernel. It applies to any `ssh`, `scp`, or `sftp` command against the NAS.
3
+ Full detail behind the always-on `rules/nas-ssh-invocation.md` kernel.
4
4
 
5
- ## Why the Windows OpenSSH binary
5
+ This file covers how to run a command there at all. What the NAS runs, where the automations are
6
+ deployed, and how to verify a change against them belong with the project that owns those
7
+ automations, not here.
6
8
 
7
- Git Bash's MSYS `ssh` reads `~/.ssh/id_ed25519` as world-readable through its ACL mapping, rejects the key as bad permissions, offers no key, and falls back to an interactive password prompt. In an unattended session no one answers that prompt, so the session hangs. The `System32/OpenSSH` binary authenticates the same key without a prompt.
8
-
9
- ## The form to use
9
+ ## The call
10
10
 
11
11
  ```
12
- "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 -p 22 operator@nas.example.local "<cmd>"
12
+ python <runner-path> <command-script.sh> <private-key-path>
13
13
  ```
14
14
 
15
- `scp` and `sftp` take the matching `System32/OpenSSH` binary, `-o BatchMode=yes`, and the same port (`-P` for `scp`).
15
+ Two arguments, both paths. The first is a file holding a bash script; the second is the private key.
16
+
17
+ The runner is `nas_ssh_key.py`. It ships with the automation project it serves, so read its path from
18
+ that project's own reference rather than assuming a location.
19
+
20
+ The host, ssh port and ssh user are constants inside the runner, so no command you write names them.
21
+ The same three values are recorded under the `nas` key in `~/.claude/local-identity.json`. Read them
22
+ from there when something needs them, and keep them out of anything committed or posted.
23
+
24
+ Write the script file with the Write tool. Keeping the commands in a file has a second benefit: a
25
+ destructive word such as `rm -rf` inside the file never appears in a Bash tool command string, so the
26
+ `destructive_command_blocker` hook stays out of the way.
27
+
28
+ The script runs through `bash -s` in a single shell. Variables, `cd`, and `source` all carry from one
29
+ line to the next. Standard output, standard error and the exit code all come back.
30
+
31
+ ## Why not ssh, scp or sftp
32
+
33
+ The runner loads the key with paramiko and signs inside the Python process. The command-line clients
34
+ check the key file's permissions first and refuse to load a key whose permissions they do not like.
35
+ Git Bash's `ssh` then falls back to an interactive password prompt, which hangs a run with nobody to
36
+ answer it.
37
+
38
+ To move a file onto the NAS, write it from inside the script:
39
+
40
+ ```bash
41
+ cat > /tmp/thing.conf <<'EOF'
42
+ contents here
43
+ EOF
44
+ ```
45
+
46
+ The quoted `'EOF'` stops the shell expanding anything in the body.
47
+
48
+ ## The key
49
+
50
+ Use the ops key under `~/.claude/keys`. It is readable as-is and the NAS accepts it.
16
51
 
17
- The host, ssh user, and port come from the `CLAUDE_NAS_*` environment variables or `~/.claude/local-identity.json`; the committed examples show placeholders (`nas.example.local`, `operator`, `22`).
52
+ Keys under `~/.ssh` are for other jobs and none of them works here. The default one carries a
53
+ passphrase, so paramiko cannot load it unattended; the rest are for other hosts.
18
54
 
19
- `-o BatchMode=yes` is required, not optional: it turns a key-authentication failure into a loud non-zero exit rather than a silent password prompt, so an auth regression surfaces as an error you can read.
55
+ ## Writing a script that touches the automations
56
+
57
+ Do not reach for the system interpreter. It carries neither pytest nor the libraries the automations
58
+ import, so anything checked against it proves nothing.
59
+
60
+ Each automation project records its own runtime — the interpreter, the virtual environment, the
61
+ import root, and the deploy path. Read that project's reference and activate what it names before
62
+ running anything.
63
+
64
+ ## Verifying a change before it ships
65
+
66
+ The NAS is a Linux box and this machine is Windows, so two classes of difference show up only there:
67
+ syntax newer than the NAS interpreter accepts, and tests that assert Windows paths.
68
+
69
+ **Always separate a real break from a platform-only one.** Run the same test file at the branch head
70
+ and at the commit the work started from. Identical failures at both mean the environment, not the
71
+ change. Report both numbers rather than the head alone.
72
+
73
+ ## The /tmp limit
74
+
75
+ `/tmp` is a small tmpfs. A full clone of a repository that carries binary assets fills it, the clone
76
+ dies partway through, and the disk stays full for whatever runs next.
77
+
78
+ Fetch shallow and sparse instead:
79
+
80
+ ```bash
81
+ d=$(mktemp -d /tmp/work.XXXXXX)
82
+ cd "$d"
83
+ git init --quiet
84
+ git remote add origin <repository-url>
85
+ git sparse-checkout init --cone
86
+ git sparse-checkout set <subdirectory>
87
+ git fetch --quiet --depth 1 origin <sha>
88
+ git checkout --quiet FETCH_HEAD
89
+ ```
90
+
91
+ One subdirectory at one commit lands tens of megabytes rather than the whole history. Clean up at the
92
+ end and print `df -h /tmp` so the next run knows the state:
93
+
94
+ ```bash
95
+ find /tmp -maxdepth 1 -name 'work.*' -prune -exec rm -rf {} +
96
+ ```
20
97
 
21
- ## Enforcement
98
+ ## When something fails
22
99
 
23
- `nas_ssh_binary_enforcer.py` (PreToolUse on Bash) denies a bare `ssh`/`scp`/`sftp` command word aimed at the NAS host and points at the full-binary form. It also denies the full `System32/OpenSSH` binary to that host when the command omits `-o BatchMode=yes`. Commands to any other host, and commands that mention the address without an ssh-family command word, pass.
100
+ | What you see | What it means |
101
+ |---|---|
102
+ | `PermissionError: [Errno 13]` on the key | The key file is not readable by this account. |
103
+ | `PasswordRequiredException: Private key file is encrypted` | That key has a passphrase and cannot be used unattended. |
104
+ | `Authentication (publickey) failed` | The transport works and the NAS does not accept that key. |
105
+ | `Load key ...: Permission denied` from `ssh.exe` | A command-line client is being used. Use the runner. |
106
+ | `No space left on device` | `/tmp` is full. Clean it, then fetch shallow and sparse. |
107
+ | `No module named pytest` | The system interpreter is being used. Activate the project's environment first. |
@@ -11,14 +11,35 @@ The gates follow the same shape as the `verified_commit` gate family.
11
11
 
12
12
  ## Opt-in (default off)
13
13
 
14
- Enforcement is **off by default**. The master flag is
15
- `CODE_REVIEW_ENFORCEMENT_ENABLED` in
16
- `hooks/blocking/config/code_review_enforcement_constants.py`. Set it to
17
- `True` to enable the push gate, the PR-create gate, the native pre-push
18
- backstop (via the shared deny decision), and the stamp-directory write
19
- blocker. When the flag is `False`, every gate allows the action and the
14
+ Enforcement is **off by default**. Turn it on by setting the environment
15
+ variable `CLAUDE_CODE_REVIEW_ENFORCEMENT` to `1`, `true`, `yes`, or `on`
16
+ (case and surrounding spaces are ignored). Any other value, and an unset
17
+ variable, leave enforcement off.
18
+
19
+ Set the variable in the machine's own user environment so every gate reads it,
20
+ including the native git pre-push backstop, which git runs in the shell's
21
+ environment. An `env` block in `settings.json` reaches the three Claude Code
22
+ hook gates alone. Each gate process reads the variable as it starts, so a
23
+ Claude Code session already running keeps its current setting until it
24
+ restarts, while the git pre-push backstop picks up the current shell
25
+ environment on each push.
26
+
27
+ A user-environment setting arms the gates machine-wide. The backstop installs
28
+ through the shared `core.hooksPath`, so it runs for every repository on the
29
+ machine, and the gates carry no per-repository allowlist. Each repository then
30
+ needs its own clean stamp before a push lands there.
31
+
32
+ The variable feeds the master flag `CODE_REVIEW_ENFORCEMENT_ENABLED` in
33
+ `hooks/blocking/config/code_review_enforcement_constants.py`, which every gate
34
+ reads at start-up. When it is on, the push gate, the PR-create gate, the native
35
+ pre-push backstop (via the shared deny decision), and the stamp-directory write
36
+ blocker all enforce. When it is off, every gate allows the action and the
20
37
  write-blocker allows stamp-directory access.
21
38
 
39
+ `npx claude-dev-env` copies the shipped `hooks/` tree over `~/.claude/` on each
40
+ install, so the environment setting survives an install and an edit to the
41
+ constant does not.
42
+
22
43
  ## How a stamp works
23
44
 
24
45
  A stamp is a small JSON file that records one fact: a clean `/code-review` pass
@@ -88,6 +109,10 @@ anchor a forgery-proof mint. The stamp reaches the same posture the
88
109
  create-PR gate.
89
110
  - **`git push --no-verify`.** This flag tells git to skip the native pre-push
90
111
  hook, so the native backstop does not run.
112
+ - **A push that clears the enable variable.** The backstop reads
113
+ `CLAUDE_CODE_REVIEW_ENFORCEMENT` from the shell that runs the push, so
114
+ `CLAUDE_CODE_REVIEW_ENFORCEMENT=0 git push` reads as enforcement off and the
115
+ backstop allows the push with no flag and no bypass marker.
91
116
  - **A rebuilt store.** A script that re-implements the stamp store in memory
92
117
  and writes a matching file can mint a stamp the gates accept.
93
118
 
@@ -82,6 +82,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
82
82
  | `destructive_command_blocker.py` | PreToolUse (Bash/PowerShell) | Shell commands with destructive literals (`rm -rf`, `git reset --hard`, etc.) |
83
83
  | `docstring_rule_gate_count_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A stale spelled-out gate-validator count in `docstring-prose-matches-implementation.md` — the "N more gate validators" / "M gated slices" count drifting from the `check_docstring_*` validators the prose names |
84
84
  | `duplicate_rmtree_helper_blocker.py` | PreToolUse (Write/Edit) | A local re-definition of the Windows-safe rmtree helper trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`) in place of importing a shared helper |
85
+ | `eli11_reply_enforcer.py` | Stop | Final replies breaking the `eli11-replies` shape — more than 120 reader-visible words, more than 6 bullet lines, more than 2 lines carrying over 20 words each, or instruction lines telling the user to act with no numbered step among the lead lines. Code fences, inline code, blockquotes, table rows, and link targets come off before the reply is judged, replies under 60 words always pass, and a reply opening with `Long form:` opts out entirely |
85
86
  | `env_var_table_code_drift_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A markdown env-var summary table row attributing an environment variable to a code file whose source never references that variable name |
86
87
  | `es_exe_path_rewriter.py` | PreToolUse | Rewrites paths referencing `.exe` under the Everything search path |
87
88
  | `gh_body_arg_blocker.py` | PreToolUse (Bash) | `gh` commands passing `--body`/`-b` directly (requires `--body-file` instead) |
@@ -108,10 +109,12 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
108
109
  | `sensitive_file_protector.py` | PreToolUse (Write/Edit) | Writes to sensitive credential or config files |
109
110
  | `session_edit_stage_gate.py` | PreToolUse (Bash) | A `git commit` that would drop files edited this session because they are tracked but left unstaged |
110
111
  | `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
112
+ | `shell_substitution_blocker.py` | PreToolUse (Bash) | A command carrying `$(...)`, a live backtick, or `<(...)`/`>(...)` process substitution, which the allowlist matcher cannot descend into |
111
113
  | `stale_comment_reference_blocker.py` | PreToolUse (Edit) | An Edit that rewrites a Python code line while keeping the standalone comment directly above it, when that comment names an identifier the rewrite removes from the line |
112
114
  | `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation |
113
115
  | `subprocess_budget_completeness.py` | PreToolUse | Subprocess calls missing required budget arguments |
114
116
  | `tdd_enforcer.py` | PreToolUse (Write/Edit) | Production code written without a matching failing test |
117
+ | `unscoped_search_blocker.py` | PreToolUse (Bash/PowerShell) | A `find` or recursive listing that walks from the filesystem root, a drive root, bare home, or a network share root |
115
118
  | `verdict_directory_write_blocker.py` | PreToolUse (Bash/PowerShell) | Shell writes into `~/.claude/verification/` |
116
119
  | `verified_commit_gate.py` | PreToolUse (Bash/PowerShell) | `git commit`/`git push` without a passing verifier verdict |
117
120
  | `verified_commit_message_accuracy_blocker.py` | PreToolUse | Commit messages that misstate what the diff has |
@@ -1,19 +1,49 @@
1
1
  """Single source of truth for the code-review enforcement gate family.
2
2
 
3
- Holds the stamp directory name, the ordered effort tokens (``low`` under
4
- ``medium`` under ``high`` under ``xhigh`` under ``max``, with ``ultra`` kept
5
- out because it needs an interactive terminal), the effort a push and a
6
- pull-request creation each require, the stamp record keys, the gate and
7
- write-blocker messages, the store-forge shell patterns, the MCP create-PR
8
- tool name, and the effort comparison every gate and the stamp store share so
9
- the thresholds never drift between them, and the master enable flag
10
- (``CODE_REVIEW_ENFORCEMENT_ENABLED``, default off) that every gate and
11
- the stamp-directory write-blocker read before they enforce anything.
3
+ ::
4
+
5
+ CLAUDE_CODE_REVIEW_ENFORCEMENT=1 -> CODE_REVIEW_ENFORCEMENT_ENABLED True
6
+ (unset) -> CODE_REVIEW_ENFORCEMENT_ENABLED False
7
+ effort_meets_threshold("xhigh", PUSH_REQUIRED_EFFORT) -> True
8
+ effort_meets_threshold("high", PR_CREATE_REQUIRED_EFFORT) -> False
9
+
10
+ Every gate reads its thresholds and messages from here, so the effort a push
11
+ needs and the effort a pull request needs never drift apart. The master flag
12
+ tracks the environment, so a machine opts in through its own environment and
13
+ the choice outlives a reinstall that rewrites this shipped file.
12
14
  """
13
15
 
14
16
  from __future__ import annotations
15
17
 
16
- CODE_REVIEW_ENFORCEMENT_ENABLED = False
18
+ import os
19
+
20
+ CODE_REVIEW_ENFORCEMENT_ENV_VAR = "CLAUDE_CODE_REVIEW_ENFORCEMENT"
21
+ ALL_ENFORCEMENT_ENABLED_ENV_VALUES = frozenset({"1", "true", "yes", "on"})
22
+
23
+
24
+ def code_review_enforcement_enabled_in_environment() -> bool:
25
+ """Read whether this machine turns the code-review gates on.
26
+
27
+ ::
28
+
29
+ CLAUDE_CODE_REVIEW_ENFORCEMENT=1 -> True
30
+ CLAUDE_CODE_REVIEW_ENFORCEMENT=" On " -> True
31
+ CLAUDE_CODE_REVIEW_ENFORCEMENT=0 -> False
32
+ (variable unset) -> False
33
+
34
+ A machine opts in through its own environment, so an install that rewrites
35
+ this shipped file leaves the choice standing. Any value outside the enabled
36
+ set, and an unset variable, read as off, so the gate family stays quiet
37
+ until someone asks for it.
38
+
39
+ Returns:
40
+ True when the variable holds an enabled value, False otherwise.
41
+ """
42
+ raw_environment_setting = os.environ.get(CODE_REVIEW_ENFORCEMENT_ENV_VAR, "")
43
+ return raw_environment_setting.strip().lower() in ALL_ENFORCEMENT_ENABLED_ENV_VALUES
44
+
45
+
46
+ CODE_REVIEW_ENFORCEMENT_ENABLED = code_review_enforcement_enabled_in_environment()
17
47
  STAMP_DIRECTORY_NAME = "code-review-stamps"
18
48
  ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER = ("low", "medium", "high", "xhigh", "max")
19
49
  PUSH_REQUIRED_EFFORT = "low"
@@ -9,6 +9,8 @@ import importlib.util
9
9
  import pathlib
10
10
  import re
11
11
 
12
+ import pytest
13
+
12
14
  _CONFIG_DIR = pathlib.Path(__file__).parent
13
15
 
14
16
  SAMPLE_ROOT_KEY_HEX_LENGTH = 16
@@ -23,7 +25,10 @@ _constants_module = importlib.util.module_from_spec(_constants_spec)
23
25
  _constants_spec.loader.exec_module(_constants_module)
24
26
 
25
27
  effort_meets_threshold = _constants_module.effort_meets_threshold
26
- CODE_REVIEW_ENFORCEMENT_ENABLED = _constants_module.CODE_REVIEW_ENFORCEMENT_ENABLED
28
+ CODE_REVIEW_ENFORCEMENT_ENV_VAR = _constants_module.CODE_REVIEW_ENFORCEMENT_ENV_VAR
29
+ code_review_enforcement_enabled_in_environment = (
30
+ _constants_module.code_review_enforcement_enabled_in_environment
31
+ )
27
32
  PUSH_REQUIRED_EFFORT = _constants_module.PUSH_REQUIRED_EFFORT
28
33
  PR_CREATE_REQUIRED_EFFORT = _constants_module.PR_CREATE_REQUIRED_EFFORT
29
34
  GATED_PUSH_SUBCOMMANDS = _constants_module.GATED_PUSH_SUBCOMMANDS
@@ -109,5 +114,53 @@ def test_guard_message_directs_users_to_the_sanctioned_minter_flag() -> None:
109
114
  assert SANCTIONED_STAMP_MINTER_FLAG in STAMP_DIRECTORY_GUARD_MESSAGE
110
115
 
111
116
 
112
- def test_enforcement_defaults_to_off() -> None:
113
- assert CODE_REVIEW_ENFORCEMENT_ENABLED is False
117
+ def test_enforcement_defaults_to_off(monkeypatch: pytest.MonkeyPatch) -> None:
118
+ monkeypatch.delenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, raising=False)
119
+ assert code_review_enforcement_enabled_in_environment() is False
120
+
121
+
122
+ @pytest.mark.parametrize("enabling_value", ["1", "true", "TRUE", " yes ", "on"])
123
+ def test_enforcement_turns_on_for_each_enabling_environment_value(
124
+ monkeypatch: pytest.MonkeyPatch, enabling_value: str
125
+ ) -> None:
126
+ monkeypatch.setenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, enabling_value)
127
+ assert code_review_enforcement_enabled_in_environment() is True
128
+
129
+
130
+ @pytest.mark.parametrize("non_enabling_value", ["0", "false", "off", "", "maybe"])
131
+ def test_enforcement_stays_off_for_a_non_enabling_environment_value(
132
+ monkeypatch: pytest.MonkeyPatch, non_enabling_value: str
133
+ ) -> None:
134
+ monkeypatch.setenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, non_enabling_value)
135
+ assert code_review_enforcement_enabled_in_environment() is False
136
+
137
+
138
+ def _module_flag_after_a_fresh_import(
139
+ monkeypatch: pytest.MonkeyPatch, environment_value: str | None
140
+ ) -> bool:
141
+ if environment_value is None:
142
+ monkeypatch.delenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, raising=False)
143
+ else:
144
+ monkeypatch.setenv(CODE_REVIEW_ENFORCEMENT_ENV_VAR, environment_value)
145
+ fresh_spec = importlib.util.spec_from_file_location(
146
+ "code_review_enforcement_constants_fresh_import",
147
+ _CONFIG_DIR / "code_review_enforcement_constants.py",
148
+ )
149
+ assert fresh_spec is not None
150
+ assert fresh_spec.loader is not None
151
+ fresh_module = importlib.util.module_from_spec(fresh_spec)
152
+ fresh_spec.loader.exec_module(fresh_module)
153
+ flag_value: bool = fresh_module.CODE_REVIEW_ENFORCEMENT_ENABLED
154
+ return flag_value
155
+
156
+
157
+ def test_module_flag_turns_on_when_the_environment_asks_for_enforcement(
158
+ monkeypatch: pytest.MonkeyPatch,
159
+ ) -> None:
160
+ assert _module_flag_after_a_fresh_import(monkeypatch, "true") is True
161
+
162
+
163
+ def test_module_flag_stays_off_when_the_environment_is_unset(
164
+ monkeypatch: pytest.MonkeyPatch,
165
+ ) -> None:
166
+ assert _module_flag_after_a_fresh_import(monkeypatch, None) is False