claude-dev-env 7.4.1 → 7.6.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 (178) hide show
  1. package/.agents/skills/everything-search/scripts/everything_search.py +11 -0
  2. package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
  3. package/.agents/skills/fresh-branch/SKILL.md +1 -0
  4. package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
  5. package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
  6. package/.agents/skills/syncing-submodules/SKILL.md +1 -0
  7. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
  8. package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
  9. package/.agents/skills/test-runner/SKILL.md +1 -0
  10. package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
  11. package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
  12. package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
  13. package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
  14. package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
  15. package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
  16. package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
  23. package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
  24. package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
  25. package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
  26. package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
  27. package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
  28. package/_shared/pr-loop/scripts/preflight.py +7 -0
  29. package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
  30. package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
  31. package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
  32. package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
  33. package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
  34. package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
  35. package/bin/install-constants.mjs +4 -2
  36. package/bin/install.cursor-rules.test.mjs +57 -15
  37. package/bin/install.mjs +10 -16
  38. package/bin/install.policy-lint.test.mjs +1 -2
  39. package/bin/install.test.mjs +0 -1
  40. package/bin/pstack-model-preferences.claude.json +23 -0
  41. package/hooks/advisory/msys_path_conversion_advisor.py +200 -0
  42. package/hooks/advisory/pr_done_reminder.py +14 -13
  43. package/hooks/advisory/refactor_guard.py +8 -0
  44. package/hooks/advisory/test_msys_path_conversion_advisor.py +223 -0
  45. package/hooks/advisory/test_pr_done_reminder.py +22 -0
  46. package/hooks/advisory/test_refactor_guard.py +40 -0
  47. package/hooks/blocking/bash_post_call_dispatcher.py +6 -11
  48. package/hooks/blocking/block_main_commit.py +4 -0
  49. package/hooks/blocking/code_rules_comments.py +39 -5
  50. package/hooks/blocking/msys_rev_path_rewriter.py +192 -0
  51. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
  52. package/hooks/blocking/precommit_code_rules_gate.py +3 -0
  53. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
  54. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
  55. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
  56. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +16 -3
  57. package/hooks/blocking/test_bash_post_call_dispatcher.py +2 -0
  58. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
  59. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
  60. package/hooks/blocking/test_msys_rev_path_rewriter.py +141 -0
  61. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
  62. package/hooks/git-hooks/pull_request_handoff.py +3 -0
  63. package/hooks/git-hooks/subprocess_window_access.py +41 -0
  64. package/hooks/git-hooks/test_verification_start.py +4 -0
  65. package/hooks/git-hooks/verification_notice_context.py +3 -0
  66. package/hooks/git-hooks/verification_start.py +7 -8
  67. package/hooks/hooks.json +10 -0
  68. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +9 -0
  69. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +6 -1
  70. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
  71. package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +48 -0
  72. package/hooks/hooks_constants/msys_rev_path_rewriter_constants.py +37 -0
  73. package/hooks/hooks_constants/post_tool_use_context.py +34 -0
  74. package/hooks/hooks_constants/pr_done_reminder_constants.py +0 -2
  75. package/hooks/hooks_constants/pre_tool_use_allow_output.py +48 -0
  76. package/hooks/hooks_constants/subprocess_window.py +141 -0
  77. package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
  78. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +9 -2
  79. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +9 -2
  80. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
  81. package/hooks/hooks_constants/test_failure_recorder_constants.py +1 -2
  82. package/hooks/hooks_constants/test_post_tool_use_context.py +22 -0
  83. package/hooks/hooks_constants/test_pre_tool_use_allow_output.py +27 -0
  84. package/hooks/hooks_constants/test_subprocess_window.py +128 -0
  85. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
  86. package/hooks/observability/test_failure_recorder.py +3 -1
  87. package/hooks/validation/mypy_validator.py +4 -0
  88. package/hooks/validators/README.md +1 -0
  89. package/hooks/validators/git_checks.py +4 -0
  90. package/hooks/validators/health_check.py +3 -0
  91. package/hooks/validators/hook_timing_harness.py +3 -0
  92. package/hooks/validators/mypy_integration.py +4 -0
  93. package/hooks/validators/ruff_integration.py +6 -0
  94. package/hooks/validators/run_all_validators.py +7 -0
  95. package/hooks/validators/subprocess_window_access.py +41 -0
  96. package/hooks/workflow/auto_formatter.py +8 -0
  97. package/hooks/workflow/test_auto_formatter.py +39 -0
  98. package/package.json +1 -1
  99. package/rules/ask-user-question-required.md +3 -3
  100. package/rules/code-standards.md +3 -2
  101. package/rules/confirm-implementation-forks.md +2 -0
  102. package/rules/destructive-commands.md +19 -19
  103. package/rules/doc-inventory-integrity.md +4 -4
  104. package/rules/docstring-prose-matches-implementation.md +2 -2
  105. package/rules/failure-blast-radius.md +4 -2
  106. package/rules/filesystem-search.md +1 -1
  107. package/rules/git-workflow.md +34 -0
  108. package/rules/long-horizon-autonomy.md +2 -0
  109. package/rules/nas-ssh-invocation.md +3 -3
  110. package/rules/no-cross-skill-duplicate-helpers.md +6 -6
  111. package/rules/orphan-css-class.md +5 -5
  112. package/rules/paired-test-coverage.md +6 -6
  113. package/rules/plain-illustrative-docstrings.md +3 -3
  114. package/rules/pstack-models.md +4 -3
  115. package/rules/re-stage-before-commit.md +7 -7
  116. package/rules/retired-hook-prose.md +16 -0
  117. package/rules/shell-invocation.md +17 -1
  118. package/rules/testing.md +3 -5
  119. package/rules/verify-runtime-state.md +1 -1
  120. package/rules/windows-filesystem-safe.md +2 -2
  121. package/rules/workflow-substitution-slots.md +1 -1
  122. package/scripts/automatic_advisory/checkout.py +8 -1
  123. package/scripts/automatic_advisory/cli.py +9 -3
  124. package/scripts/automatic_advisory/config/constants.py +0 -2
  125. package/scripts/automatic_advisory/execution.py +7 -1
  126. package/scripts/automatic_advisory/git.py +9 -1
  127. package/scripts/automatic_advisory/process_host.py +3 -0
  128. package/scripts/claude_chain_runner.py +3 -0
  129. package/scripts/durable_post_lint_config/config/constants.py +0 -3
  130. package/scripts/durable_post_lint_config/config/test_constants.py +5 -0
  131. package/scripts/gh_artifact_upload.py +3 -0
  132. package/scripts/grok_headless_runner.py +3 -0
  133. package/scripts/grok_patch_artifacts.py +4 -0
  134. package/scripts/grok_worker_preflight.py +3 -0
  135. package/scripts/invoke_code_review.py +3 -0
  136. package/scripts/local_verification/command_runner.py +3 -0
  137. package/scripts/local_verification/git_state.py +3 -0
  138. package/scripts/policy_lint/adapter_configuration.py +24 -0
  139. package/scripts/policy_lint/adapter_detectors.py +48 -2
  140. package/scripts/policy_lint/adapter_pairing.py +150 -2
  141. package/scripts/policy_lint/adapter_retired_hook_prose.py +201 -0
  142. package/scripts/policy_lint/adapters.py +82 -5
  143. package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
  144. package/scripts/policy_lint/config/constants.py +77 -0
  145. package/scripts/policy_lint/registry.py +6 -1
  146. package/scripts/policy_lint/selection.py +37 -18
  147. package/scripts/policy_lint/selection_git.py +17 -2
  148. package/scripts/repository_checks/config/constants.py +5 -0
  149. package/scripts/repository_checks/config/policy_document.py +19 -0
  150. package/scripts/repository_checks/email_exemptions.py +14 -71
  151. package/scripts/repository_checks/path_exemptions.py +35 -0
  152. package/scripts/repository_checks/policy_document.py +120 -0
  153. package/scripts/repository_checks/tracked_secrets.py +27 -4
  154. package/scripts/setup_project_paths.py +3 -0
  155. package/scripts/spawn_grok_batch.py +3 -0
  156. package/scripts/subprocess_window_access.py +41 -0
  157. package/scripts/test_dispatcher_profile_import.py +4 -0
  158. package/scripts/test_durable_post_lint.py +42 -9
  159. package/scripts/tests/test_adapter_configuration.py +8 -0
  160. package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
  161. package/scripts/tests/test_email_exemptions.py +37 -0
  162. package/scripts/tests/test_path_exemptions.py +164 -0
  163. package/scripts/tests/test_policy_document.py +104 -0
  164. package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
  165. package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
  166. package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
  167. package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +197 -0
  168. package/scripts/tests/test_policy_lint_selection.py +141 -0
  169. package/scripts/tests/test_subprocess_window_access.py +118 -0
  170. package/scripts/tests/test_tracked_secrets.py +79 -0
  171. package/scripts/validate_instruction_pairs.py +3 -0
  172. package/scripts/verify_installable_package.py +5 -0
  173. package/hooks/hooks_constants/convergence_branch_constants.py +0 -8
  174. package/hooks/hooks_constants/destructive_command_environment_constants.py +0 -18
  175. package/hooks/hooks_constants/test_text_stripping.py +0 -39
  176. package/hooks/hooks_constants/text_stripping.py +0 -36
  177. package/scripts/automatic_advisory/window_flags.py +0 -41
  178. package/scripts/repository_checks/config/email_exemptions.py +0 -10
@@ -1,41 +1,41 @@
1
1
  # Destructive Commands in Bash
2
2
 
3
- The `destructive_command_blocker` PreToolUse hook watches every Bash-tool command and matches destructive patterns (`rm -rf`, `git reset --hard`, `dd`, `mkfs`, `chmod -R`, fork bombs) as raw text, with no quote-awareness. Anything it cannot prove safe falls through to a confirmation prompt. In a background or auto-mode run no human can answer that prompt, so the call stalls.
3
+ No hook watches Bash commands for destructive patterns. What you face is the harness permission prompt. The harness raises it from the session permission mode and the permission rules in effect on your host. In a background or auto-mode run no human can answer that prompt, so the call stalls.
4
4
 
5
- Two consequences follow: use an allowed removal form, and keep a destructive literal out of the command string even when it rides only as data.
5
+ Two consequences follow. Use an allowed removal form, and keep a destructive literal out of the command string even when it rides only as data.
6
6
 
7
- ## Removal forms that never prompt
7
+ ## Removal forms to prefer
8
8
 
9
- - **Scratch and probe files** — the PowerShell tool: `Remove-Item -Recurse -Force -Confirm:$false <absolute path>`. The hook watches only the Bash tool, so a PowerShell removal never reaches it.
10
- - **Worktrees** — `git worktree remove --force <path>`. This matches no destructive pattern.
11
- - **Tracked files** — `git rm <path>`, which records the deletion in the index.
12
- - **Bash `rm` when unavoidable** — one standalone `rm`, absolute literal paths, no chaining, no globs, every target inside the ephemeral namespace below.
9
+ - **Scratch and probe files.** Use the PowerShell tool with `Remove-Item -Recurse -Force -Confirm:$false <absolute path>`.
10
+ - **Worktrees.** Use `git worktree remove --force <path>`.
11
+ - **Tracked files.** Use `git rm <path>`, which records the deletion in the index.
12
+ - **Bash `rm` when unavoidable.** Write one standalone `rm` with absolute literal paths, no chaining, and no globs. Keep every target inside the ephemeral namespace below.
13
13
 
14
- ## The ephemeral namespace the hook auto-allows
14
+ ## The ephemeral namespace
15
15
 
16
- An `rm` is auto-allowed when it is a single invocation and every target resolves inside one of:
16
+ Keep a Bash `rm` to targets that resolve inside one of these:
17
17
 
18
- - The OS temporary root (`tempfile.gettempdir()`).
18
+ - The OS temporary root.
19
19
  - A path rooted at `/tmp` or `/temp`, drive-letter tolerant.
20
20
  - A path holding a `/worktrees/` or `/worktree/` segment, or a directory git reports inside a worktree admin directory.
21
21
  - `~/.claude`.
22
22
 
23
- A bare ephemeral root (`/tmp`, the OS temp root itself, a bare directory named `worktrees` or `worktree`) is refused, so a single stray argument cannot wipe the whole namespace.
23
+ Never pass a bare ephemeral root, such as `/tmp`, the OS temp root itself, or a bare directory named `worktrees` or `worktree`. A single stray argument then wipes the whole namespace.
24
24
 
25
- Four environment variables resolve inside a target token: `TEMP`, `TMP`, `TMPDIR`, and `CLAUDE_JOB_DIR`. Any other variable, a `$(...)` or backtick expansion, or a brace glob makes the target unresolvable and the command prompts. A `$CLAUDE_JOB_DIR` path is auto-allowed only when it resolves into the namespace above — the variable is readable, not a blanket pass. Set `CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW` to a truthy value to turn the whole auto-allow off.
25
+ Write each target as a literal path. A variable, a `$(...)` or backtick expansion, or a brace glob hides what the command will delete from the reader and from the permission matcher.
26
26
 
27
- A file left in the OS temp directory or under `$CLAUDE_JOB_DIR` is cleaned by the harness and needs no explicit removal — see the exception clause in [`cleanup-temp-files.md`](cleanup-temp-files.md).
27
+ A file left in the OS temp directory or under `$CLAUDE_JOB_DIR` is cleaned by the harness and needs no explicit removal. See the exception clause in [`cleanup-temp-files.md`](cleanup-temp-files.md).
28
28
 
29
29
  ## Keep destructive literals out of the command string
30
30
 
31
- A destructive literal carried only as data — a commit message, a PR or issue body, an echoed string, a `python -c` / `node -e` / `awk` argument, a heredoc — trips the same raw-text match even though the shell never executes it.
31
+ The permission matcher reads the raw command string. A destructive literal carried only as data still sits in that string, so it can push the command out of an allowed shape and into a prompt even though the shell never executes it. This covers a commit message, a PR or issue body, an echoed string, a `python -c` or `node -e` or `awk` argument, and a heredoc.
32
32
 
33
- - Bodies that describe destructive-command behavior go in a file passed by path: `git commit -F <file>`, `gh … --body-file <file>` (see [`gh-cli-conventions.md`](gh-cli-conventions.md)). Never `git commit -m` or `gh … -b`.
34
- - To exercise or verify the blocker, or any hook, run the committed test suite (`python -m pytest <test_file>`), which passes the command strings as in-language data. Never an inline `python -c` harness.
33
+ - Bodies that describe destructive-command behavior go in a file passed by path, such as `git commit -F <file>` or `gh … --body-file <file>`. See [`gh-cli-conventions.md`](gh-cli-conventions.md). Never `git commit -m` or `gh … -b`.
34
+ - To exercise or verify a hook, run the committed test suite with `python -m pytest <test_file>`, which passes the command strings as in-language data. Never an inline `python -c` harness.
35
35
 
36
36
  ## Every subagent prompt carries the rule
37
37
 
38
- A prompt-delivered directive reaches only the agent that gets it. An agent that spawns its own workers — review lenses, fix agents, verifiers — copies this line into every subagent prompt it issues, so a grandchild cleaning up its own probe file uses an allowed form:
38
+ A prompt-delivered directive reaches only the agent that gets it. An agent that spawns its own workers, such as review lenses, fix agents, or verifiers, copies this line into every subagent prompt it issues. A grandchild cleaning up its own probe file then uses an allowed form:
39
39
 
40
40
  > Never use bash rm in any form. Delete scratch/probe files with the PowerShell tool (Remove-Item -Recurse -Force -Confirm:$false <absolute path>), or leave them in the OS temp dir; remove worktrees only via git worktree remove --force.
41
41
 
@@ -43,5 +43,5 @@ Prefer that a child leaves its scratch files for the parent to remove at teardow
43
43
 
44
44
  ## Sibling rules
45
45
 
46
- - [`cleanup-temp-files.md`](cleanup-temp-files.md) — which scratch files a task removes, and which it leaves.
47
- - [`windows-filesystem-safe.md`](windows-filesystem-safe.md) — the safe `rmtree` / `force_rmtree` patterns for read-only Windows files.
46
+ - [`cleanup-temp-files.md`](cleanup-temp-files.md) names which scratch files a task removes, and which it leaves.
47
+ - [`windows-filesystem-safe.md`](windows-filesystem-safe.md) holds the safe `rmtree` and `force_rmtree` patterns for read-only Windows files.
@@ -11,13 +11,13 @@ paths:
11
11
 
12
12
  # Documentation Inventory Integrity
13
13
 
14
- A doc that inventories code is a contract: a reader trusts the listing to map the directory, trusts a shown command to run, and trusts a table row to name the file that reads the variable. Three hooks hold the three inventory shapes in step with the code.
14
+ A doc that inventories code is a contract: a reader trusts the listing to map the directory, trusts a shown command to run, and trusts a table row to name the file that reads the variable. Three repository checks hold the three inventory shapes in step with the code. No write-time hook runs them. Run `python packages/claude-dev-env/scripts/repository_policy.py` before you commit, and CI runs the same command.
15
15
 
16
16
  ## 1. A per-directory `CLAUDE.md` names files that exist
17
17
 
18
18
  Every bare filename a per-directory `CLAUDE.md` names points at a file in the subtree that `CLAUDE.md` describes — both the filenames its table cells list and the scripts its fenced run commands invoke (`python script.py`). Add the row and the run command in the change that adds the file; drop both in the change that removes it.
19
19
 
20
- `claude_md_orphan_file_blocker.py` (PreToolUse on Write|Edit|MultiEdit of any `CLAUDE.md`) reads the content the tool would leave on disk. For an Edit or MultiEdit it reconstructs the post-edit file and notes which orphans the file already held, so a pre-existing orphan on an untouched line is excluded and only an orphan the edit introduces is reported; when the existing file cannot be read it scans the raw `new_string` fragments instead.
20
+ `repository_checks/claude_md.py` scans every tracked `CLAUDE.md` in the committed tree and reports each orphan it holds, loading its detection logic from `claude_md_orphan_file_blocker.py`. It reads committed files rather than a pending write, so it reports a pre-existing orphan alongside a new one.
21
21
 
22
22
  It collects two kinds of reference:
23
23
 
@@ -32,7 +32,7 @@ The check stays quiet for a target that is not a `CLAUDE.md`, for a cell holding
32
32
 
33
33
  A package directory that documents its own files in a `README.md` Layout table, a `CLAUDE.md` "Key files" list, or a skill `SKILL.md` Layout table keeps that inventory in step with the directory. A new production file in such a directory gets its entry — a table row or a list bullet naming the file in backticks and saying what it does — in the same change.
34
34
 
35
- `package_inventory_stale_blocker.py` (PreToolUse on Write) blocks a new production file whose basename appears in no present inventory and names the fix. A skill `SKILL.md` Layout table that maps `scripts/` counts as the inventory for files in that subdirectory.
35
+ `repository_checks/package_inventory.py` scans the committed tree and reports a production file whose basename appears in no present inventory, loading its detection logic from `package_inventory_stale_blocker.py`. It names the fix. A skill `SKILL.md` Layout table that maps `scripts/` counts as the inventory for files in that subdirectory.
36
36
 
37
37
  Two free-prose slices stay with judgment and belong in the same change:
38
38
 
@@ -45,4 +45,4 @@ This is the `category-o-docstring-vs-impl-drift` (O8) orphaned-doc-claim shape a
45
45
 
46
46
  Every row in an env-var summary table pairs an UPPER_SNAKE variable with a code-file path that reads it — written as `` | `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | … | ``. When a code change removes the last read of a variable from a file, the same change drops or corrects the row naming that file.
47
47
 
48
- `env_var_table_code_drift_blocker.py` (PreToolUse on Write|Edit|MultiEdit of `.md`) blocks a row whose named code file exists yet never references the variable, and names the fix. For an Edit, drift a file already held on an untouched row is excluded; a row whose code file resolves nowhere stays quiet, since the hook cannot prove the drift.
48
+ `repository_checks/env_var_documentation.py` scans tracked `.md` files and reports a row whose named code file exists yet never references the variable, loading its detection logic from `env_var_table_code_drift_blocker.py`. It names the fix. A row whose code file resolves nowhere stays quiet, since the check cannot prove the drift.
@@ -28,13 +28,13 @@ Read the body and the docstring side by side. Apply each check that matches the
28
28
  - **Gate-outcome status flags** — an outcome routed to a blocker (`blocker = ...; break`) reads as blocked everywhere, never as a bypass.
29
29
  - **Returns / Raises / Note claims** — each free-form claim matches the body.
30
30
 
31
- Many deterministic shapes of this drift have Write/Edit gates in `~/.claude/hooks/blocking/code_rules_docstrings.py` (and the JS/`.mjs` slices in `code_rules_imports_logging.py`). Free-form rest is judgment.
31
+ Many deterministic shapes of this drift are checked in `code_rules_docstrings.py` (and the JS and `.mjs` slices in `code_rules_imports_logging.py`). The staged policy lint reaches both through `code_rules_enforcer.py`, and no write-time hook runs them. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. Free-form rest is judgment.
32
32
 
33
33
  ## Hook prose matches its detector
34
34
 
35
35
  A hook module is the sharpest case of the same rule: its docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags, and claim no broader trigger surface than the regex enforces.
36
36
 
37
- `hook_prose_detector_consistency` (PreToolUse on Write|Edit|MultiEdit of hook modules and their `*_constants.py` companions) blocks prose that claims a trigger the detector never fires on, and names the fix.
37
+ The staged policy lint carries this as its `hook-prose-consistency` rule, covering hook modules and their `*_constants.py` companions. It reports prose that claims a trigger the detector never fires on, and names the fix. No write-time hook runs it, so run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit.
38
38
 
39
39
  After writing a hook, ask: would a token matching every word of this message actually trip the detector? When the message names a shape the regex skips, rewrite the message to name only what the regex catches. The path-shape case is the common overstatement — a detector that keys off a path separator must not claim it blocks an "output-key segment". The corrective message spells the rewrite.
40
40
 
@@ -64,9 +64,11 @@ The report then presents each candidate to the owner for a durable-fix decision,
64
64
 
65
65
  ## Enforcement
66
66
 
67
- `code_rules_blast_radius.py` (PreToolUse on Write and Edit, hosted by `code_rules_enforcer.py`) requires each raised type written directly inside a loop body to end in `RunFatal` or `ItemBlocked`. The lexical check covers raises written directly in loop bodies. Shared helpers carry multiple caller contexts, so their callers classify the boundary.
67
+ `code_rules_blast_radius.py` runs inside `code_rules_enforcer.py`, which the staged policy lint applies to each changed file under its `code-rules` rule. No write-time hook runs it, so run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base.
68
68
 
69
- Findings use baseline content for each edit. A raise present on disk remains accepted during the edit; the gate evaluates newly written raises.
69
+ The check requires each raised type written directly inside a loop body to end in `RunFatal` or `ItemBlocked`. The lexical check covers raises written directly in loop bodies. Shared helpers carry multiple caller contexts, so their callers classify the boundary.
70
+
71
+ Findings compare each changed file against its baseline. A raise already present in the baseline stays accepted, so the check reports only newly written raises.
70
72
 
71
73
  ## Excerpt for repository-instruction sessions
72
74
 
@@ -47,5 +47,5 @@ Issue one shell search at a time when the walk is large. Parallel full-tree sear
47
47
 
48
48
  ## Search controls
49
49
 
50
- - `unscoped_search_blocker` (PreToolUse on Bash and PowerShell, hosted by `bash_pre_tool_use_dispatcher`) denies a walk from an unscoped root and returns the scoped alternative.
50
+ - No hook denies a walk from an unscoped root. `unscoped_search_blocker` went with the blocking policy hooks in `0f21faf8e`, which left the Bash PreToolUse roster empty. The scope invariant above is guidance a reader follows.
51
51
  - For Everything searches that use a project name, run `python "${CLAUDE_SKILL_DIR}/scripts/everything_search.py" <project-name> <search arguments>`. An exact project name becomes its path from `~/.claude/project-paths.json`. The command then starts `es.exe` without a shell. `scripts/setup_project_paths.py` writes the registry.
@@ -50,6 +50,40 @@ untouched.
50
50
  - Before push: `gh pr ready --undo`
51
51
  - After review approved: `gh pr ready`
52
52
 
53
+ ## Run the required checks locally before the first push
54
+
55
+ Read the branch ruleset for the required check contexts before you push a branch,
56
+ or any level of a stack: `gh api repos/<owner>/<repo>/rules/branches/<trunk>`.
57
+ Then run that exact gate command locally against that level's own base, on every
58
+ level. A required check that never fired is invisible debt at every level, and it
59
+ surfaces only after the whole stack is pushed, when the repair costs a second pass
60
+ over every branch.
61
+
62
+ A checks listing that reports nothing on the branch is a finding, not a neutral
63
+ state. Find out whether the workflow's event filters exclude the branch, or whether
64
+ the check simply never ran, before you treat that branch as clean.
65
+
66
+ ## Each stack level stands on its own
67
+
68
+ A symbol belongs at the level that first **uses** it, not the level that first
69
+ mentions it. A bottom pull request that declares the imports its descendants will
70
+ need fails the linter on unused imports. A test helper that calls a function three
71
+ levels above it fails on an undefined name. Both defects stay invisible while you
72
+ read the finished tip, and both are obvious the moment you check one level alone.
73
+
74
+ Prove each level before you push it: import the modules that level changes, and run
75
+ the required linter against that level's own base. To repair a level, rebuild its
76
+ import header as the union of what that level references, let the linter's
77
+ autofix strip the rest, and move a premature helper up to the level that defines
78
+ what it calls.
79
+
80
+ ## A force-push that moves content obliges a description refresh
81
+
82
+ Force-with-lease protects the ref. It protects nobody's understanding of what the
83
+ branch now holds. When a rewrite moves content between levels of a stack, or
84
+ otherwise changes what a branch contains, refresh that pull request's description
85
+ through the `pr-description-writer` agent before you ask anyone to read or merge it.
86
+
53
87
  ## Never commit working documents or images
54
88
 
55
89
  **NEVER commit these files to the repo:**
@@ -18,6 +18,8 @@ Before you end any turn, read your last paragraph. If it is a plan, an analysis,
18
18
 
19
19
  In an autonomous pipeline the user cannot answer mid-task. For reversible actions that follow from the original request, act without asking; save any follow-up offers for after the task is done.
20
20
 
21
+ Authority the task already granted stays granted. A later preference about tone, brevity, or reply format changes how you report, never what you are allowed to do. Re-asking for permission the task already gave hands the work back to the user, who then does it by hand.
22
+
21
23
  ## Delegate and keep working
22
24
 
23
25
  Hand independent subtasks to subagents and keep working while they run; let them run in the background rather than block until each one returns. Reuse a long-lived subagent across related subtasks so its context carries forward and saves repeated reads. Step in when a subagent drifts off track or is missing context.
@@ -16,9 +16,9 @@ which cannot be answered unattended, or belong to other hosts.
16
16
 
17
17
  The runner loads the key with paramiko and signs in the same process. The command-line clients check
18
18
  the key file's permissions first and refuse it, and Git Bash's `ssh` then falls back to a password
19
- prompt that hangs an unattended run. `nas_ssh_binary_enforcer.py` (PreToolUse on Bash) denies a bare
20
- ssh-family word aimed at the NAS, and denies the full `System32/OpenSSH` binary when
21
- `-o BatchMode=yes` is missing.
19
+ prompt that hangs an unattended run. No hook denies an ssh-family word aimed at the NAS, so keep to
20
+ the runner yourself. When the `System32/OpenSSH` binary is unavoidable, pass `-o BatchMode=yes` so it
21
+ fails fast on a refused key.
22
22
 
23
23
  Host, ssh port and ssh user are constants inside the runner and are also under the `nas` key in
24
24
  `~/.claude/local-identity.json`. Keep all three out of anything committed or posted.
@@ -8,7 +8,7 @@ paths: "**/skills/*/scripts/**/*.py"
8
8
 
9
9
  ## The two duplication cases differ
10
10
 
11
- CODE_RULES "Reuse before create" / DRY says one helper lives in one home and both call sites import it. That rule is blocking **within one skill** — two `.py` modules in the same skill's `scripts/` directory that carry the same top-level function body fail the `code_rules_duplicate_body` gate, and the fix is a shared module both import.
11
+ CODE_RULES "Reuse before create" / DRY says one helper lives in one home and both call sites import it. That rule is enforced **within one skill**. Two `.py` modules in the same skill's `scripts/` directory that carry the same top-level function body fail the `code_rules_duplicate_body` check, which the staged policy lint runs through `code_rules_enforcer.py`. The fix is a shared module both import.
12
12
 
13
13
  Across **two skill folders** the same copy is a different call. Each skill folder installs on its own, so a shared module would couple two skills the install model keeps separate: deleting or reinstalling one skill would break a helper the other depends on. A small launch helper copied into each skill (for example a Chrome-open helper that reads the registry and runs `chrome.exe`) is a defensible skill-isolation tradeoff, not a regression.
14
14
 
@@ -16,14 +16,14 @@ Across **two skill folders** the same copy is a different call. Each skill folde
16
16
 
17
17
  Before you copy a top-level helper from one skill's `scripts/` directory into another:
18
18
 
19
- - **Same skill, two modules** — extract one shared module in that skill and import it from both. The `code_rules_duplicate_body` gate blocks the copy.
20
- - **Two skill folders, a small self-contained helper** — copy it; the skill-isolation tradeoff stands. A non-blocking `[CODE_RULES advisory]` names the source skill at Write time so the copy is a deliberate choice on record, not an oversight.
19
+ - **Same skill, two modules.** Extract one shared module in that skill and import it from both. The `code_rules_duplicate_body` check reports the copy.
20
+ - **Two skill folders, a small self-contained helper.** Copy it, and the skill-isolation tradeoff stands. An advisory `[CODE_RULES advisory]` line names the source skill when the lint runs, so the copy is a deliberate choice on record, not an oversight.
21
21
  - **Two skill folders, a large or behavior-bearing body** — when the copied body is large, holds business logic, or would drift in a way that changes behavior, raise the choice through `AskUserQuestion`: copy and accept drift, or stand up a shared dependency both skills declare (for example a published package both `requirements` files name, or a `_shared` module the install step writes into each skill). A shared dependency that survives independent install is the only shared-home path that does not break the install model.
22
22
 
23
23
  ## What the advisory tells you
24
24
 
25
- The `advise_cross_skill_duplicate_helper` check in `code_rules_duplicate_body` prints to stderr (never blocks) when a top-level function in the file being written has the same normalized body as a top-level function in another skill's `scripts/` directory. The message names the source skill and function so a reviewer can confirm the copy was intentional. It fires only across skill folders; within one skill the blocking gate already covers the copy.
25
+ The `advise_cross_skill_duplicate_helper` check in `code_rules_duplicate_body` prints to stderr (never fails the lint) when a top-level function in a changed file has the same normalized body as a top-level function in another skill's `scripts/` directory. The message names the source skill and function so a reviewer can confirm the copy was intentional. It fires only across skill folders. Within one skill the reporting check already covers the copy.
26
26
 
27
- ## Why this is a rule, not a wider gate
27
+ ## Why this is a rule, not a wider check
28
28
 
29
- Extending the blocking duplicate-body gate to span skill folders would deny the exact skill-isolation copy that keeps skills independently installable — a false positive on a sanctioned pattern. The boundary between "same skill, block" and "two skills, signal" is a judgment the writer makes with the source skill named in front of them. The rule states the judgment; the `[CODE_RULES advisory]` surfaces the signal; neither blocks the defensible copy.
29
+ Extending the duplicate-body check to span skill folders would report the exact skill-isolation copy that keeps skills independently installable, a false positive on a sanctioned pattern. The boundary between "same skill, report" and "two skills, signal" is a judgment the writer makes with the source skill named in front of them. The rule states the judgment, and the `[CODE_RULES advisory]` line surfaces the signal. Neither one fails the defensible copy.
@@ -13,16 +13,16 @@ Every class name a markup string references has a matching `.<class>` selector i
13
13
 
14
14
  When you add a `class="..."` attribute, add its `.<class>` selector to the `<style>` block in the same change. When you drop a selector, drop the class attribute it styled.
15
15
 
16
- ## What the gate checks
16
+ ## What the check covers
17
17
 
18
- The `check_orphan_css_classes` check in `code_rules_orphan_css_class.py` (wired into `code_rules_enforcer.py`) runs on every production Python write. It:
18
+ The `check_orphan_css_classes` check in `code_rules_orphan_css_class.py` reaches production Python through the staged policy lint, whose `code-rules` rule loads `code_rules_enforcer.py` and applies it to each changed file. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base. It:
19
19
 
20
20
  1. Collects each class name referenced in a `class="..."` attribute across the file's string literals.
21
21
  2. Collects each class selector defined in a `<style>` block — both in the file under edit and in every Python module beside it (its own directory and immediate child directories), since a markup module commonly imports its style constant from a companion package directory.
22
22
  3. Flags each referenced class with no matching selector in that whole set.
23
23
 
24
- The check stays quiet for a file that emits no `class="..."` markup, and for a file whose markup has no `<style>` source nearby (its stylesheet lives outside the scan, so the gate cannot judge it). Test files are exempt, since a fixture may carry intentional orphan markup.
24
+ The check stays quiet for a file that emits no `class="..."` markup, and for a file whose markup has no `<style>` source nearby (its stylesheet lives outside the scan, so the check cannot judge it). Test files are exempt, since a fixture may carry intentional orphan markup.
25
25
 
26
- ## Why this is a hook, not a lint pass
26
+ ## Why this check is mechanical
27
27
 
28
- A class attribute with no matching selector reads as styled but renders unstyled. Native elements such as `<details>` stay functional without CSS, so the gap survives review as a cosmetic defect rather than a crash — exactly the class of issue that slips past a manual pass and lands as a deferred code-standard finding. Catching it at Write time keeps the markup and the stylesheet in step as each line is written.
28
+ A class attribute with no matching selector reads as styled but renders unstyled. Native elements such as `<details>` stay functional without CSS, so the gap survives review as a cosmetic defect rather than a crash. That is the class of issue a manual pass slips past, and it lands later as a deferred code-standard finding. Running the lint on every staged change keeps the markup and the stylesheet in step.
@@ -16,9 +16,9 @@ Every public function a production module defines is exercised by a test in the
16
16
 
17
17
  When you add a public function to a module whose test suite already exercises that module — covering a sibling public function, or testing one of its private helpers — add a behavioral test that calls the new function and asserts on its return value or side effect — in the same change that adds the function. A suite that exercises only a private helper (such as a color-conversion helper) while leaving the module's public renderers untested is the exact gap this rule closes. This is the function-level half of the project rule "Every new production code path gets a paired behavioral test ... call the path and assert on what it does."
18
18
 
19
- ## What the gate checks
19
+ ## What the check covers
20
20
 
21
- Two complementary checks in `code_rules_paired_test.py` (both dispatched from `code_rules_enforcer.py`) cover the two write orders.
21
+ Two complementary checks in `code_rules_paired_test.py` reach changed files through `code_rules_enforcer.py`, which the staged policy lint runs under its `code-rules` rule. No write-time hook runs them, so run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base. The two checks cover the two write orders.
22
22
 
23
23
  `check_public_function_missing_paired_test` runs on a production Python write or edit and flags a public function when all of these hold:
24
24
 
@@ -31,10 +31,10 @@ Two complementary checks in `code_rules_paired_test.py` (both dispatched from `c
31
31
 
32
32
  A public function counts as covered when its name appears — imported, called, or named — in any `test_*.py` or `*_test.py` file in the suite directory, so a function exercised by a differently-named sibling test still counts. `main` and underscore-prefixed functions are never required to carry a test.
33
33
 
34
- ## Relationship to the file-level TDD gate
34
+ ## Relationship to the file-level TDD order
35
35
 
36
- `tdd_enforcer.py` requires a fresh test file to exist before a production module is written; it judges coverage one file at a time. This check judges coverage one function at a time for a module that already carries such a test file. The two compose: the file-level gate ensures a test file exists, and this check ensures that file covers every public function the module exposes.
36
+ `tdd_enforcer.py` once required a fresh test file to exist before a production module was written, judging coverage one file at a time. Nothing runs it now, and the staged policy lint carries no replacement, so the test-first order is yours to hold. This check judges coverage one function at a time for a module that already carries such a test file. Write the test file first, and the lint then reports any public function that file leaves uncovered.
37
37
 
38
- ## Why this is a hook, not a lint pass
38
+ ## Why this check is mechanical
39
39
 
40
- A public function with no test reads as covered when the module's test file sits right beside it and exercises its siblings. The gap survives review because the suite looks complete. Catching it as the function is written keeps the module's public surface and its test suite in step.
40
+ A public function with no test reads as covered when the module's test file sits right beside it and exercises its siblings. The gap survives review because the suite looks complete. Running the lint on every staged change keeps the module's public surface and its test suite in step.
@@ -86,6 +86,6 @@ one — and you're debugging blind.
86
86
 
87
87
  Two surfaces carry this standard:
88
88
 
89
- - **Hook (the run-on backstop).** `check_docstring_runon_sentence` in `~/.claude/hooks/blocking/code_rules_docstrings.py` flags the one mechanical mark of a wall: a single narrative sentence that is both over the word limit and joined by an em-dash or a semicolon. A hook cannot judge whether prose paints a picture, so it catches only this structural mark. It reads the narrative through a shared partition that sets aside any `::` literal block and any doctest, so a diagram's own arrows and dashes never count against the sentence.
90
- - **Hook (the prose-wall backstop).** `check_docstring_prose_wall_without_illustration` in the same module flags a narrative that runs more than six prose lines with no diagram block. It marks the wall so the writer shows the behavior with a `::` example or a doctest and trims the prose to a few short lines. It cannot judge whether the diagram illustrates well; that stays with the audit lane.
91
- - **Audit (the judgment lane).** Category O sub-bucket O9 in `~/.claude/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md` carries the illustrative-and-brief judgment the hook cannot. The audit teammate reads each changed docstring's narrative and asks whether a general developer follows it on the first read.
89
+ - **Lint (the run-on backstop).** `check_docstring_runon_sentence` in `code_rules_docstrings.py` flags the one mechanical mark of a wall, a single narrative sentence that is both over the word limit and joined by an em-dash or a semicolon. The staged policy lint reaches it through `code_rules_enforcer.py`, and no write-time hook runs it. A mechanical check cannot judge whether prose paints a picture, so it catches only this structural mark. It reads the narrative through a shared partition that sets aside any `::` literal block and any doctest, so a diagram's own arrows and dashes never count against the sentence.
90
+ - **Lint (the prose-wall backstop).** `check_docstring_prose_wall_without_illustration` in the same module flags a narrative that runs more than six prose lines with no diagram block. It marks the wall so the writer shows the behavior with a `::` example or a doctest and trims the prose to a few short lines. It cannot judge whether the diagram illustrates well, and that stays with the audit lane.
91
+ - **Audit (the judgment lane).** Category O sub-bucket O9 in `~/.claude/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md` carries the illustrative-and-brief judgment the lint cannot. The audit teammate reads each changed docstring's narrative and asks whether a general developer follows it on the first read.
@@ -28,9 +28,10 @@ The file contains this shape:
28
28
  ```
29
29
 
30
30
  Offer only model IDs the active native tool accepts. Keep the user's order.
31
- Treat `inherit-parent` and `auto` as parent inheritance, not model IDs. A
32
- different host starts with its own confirmed choices. It never copies the Codex
33
- preference file.
31
+ Treat `inherit-parent` and `auto` as parent inheritance, not model IDs. A full
32
+ install seeds the Claude file and the Codex file from the package when a host
33
+ file is absent, and leaves an existing file untouched. A different host never
34
+ copies another host's preference file.
34
35
 
35
36
  ## Selection before every delegation
36
37
 
@@ -2,15 +2,15 @@
2
2
 
3
3
  Stage the files you edited this session right before you commit them. A plain `git commit` records only the staged snapshot; a tracked file this session changed but left unstaged stays behind in the working tree.
4
4
 
5
- `session_edit_stage_gate` (PreToolUse on Bash `git commit`) denies a commit that would drop tracked session edits and names the fix: `git add <paths>`, `git commit -a`, or a `# partial-commit` marker.
5
+ No hook denies a commit that would drop tracked session edits. Run `git status` before you commit, then stage what you changed with `git add <paths>` or commit with `git commit -a`.
6
6
 
7
- Staging covers tracked files you edited. Do not commit untracked files unless the user explicitly instructs it — an untracked file in the working tree is outside the change until they say otherwise.
7
+ Staging covers tracked files you edited. Do not commit untracked files unless the user explicitly instructs it. An untracked file in the working tree is outside the change until they say otherwise.
8
8
 
9
- ## Escapes the denial does not restate
9
+ ## Staging shapes
10
10
 
11
- - **A pathspec** — `git commit -- <paths>` or `git commit <paths>` commits only the named paths on purpose and steps the gate aside.
12
- - **A preceding `git add` / `git stage`** — `git add <paths> && git commit …` stages the files in its own segment before the commit runs.
11
+ - **A pathspec.** `git commit -- <paths>` or `git commit <paths>` commits only the named paths on purpose.
12
+ - **A preceding `git add` or `git stage`.** `git add <paths> && git commit …` stages the files in its own segment before the commit runs.
13
13
 
14
- A `--amend` does not step the gate aside: an amend records the staged snapshot too, so an unstaged session edit is dropped the same way a plain commit drops it.
14
+ A `--amend` carries the same risk. An amend records the staged snapshot too, so an unstaged session edit is dropped the same way a plain commit drops it.
15
15
 
16
- `session_file_edit_tracker` (PostToolUse) records each Write/Edit/MultiEdit path; `session_edit_tracker_cleanup` (SessionStart, SessionEnd) clears the session's tracker.
16
+ `session_file_edit_tracker` (PostToolUse) still records each Write/Edit/MultiEdit path, and `session_edit_tracker_cleanup` (SessionStart, SessionEnd) still clears the session's tracker. No gate reads that record today, so it serves as history rather than a precondition.
@@ -0,0 +1,16 @@
1
+ ---
2
+ paths:
3
+ - "**/rules/*.md"
4
+ ---
5
+
6
+ # Rules Prose Names Only Hooks That Run
7
+
8
+ A hook module runs when `hooks/hooks.json` registers it or a dispatcher roster hosts it. A module can survive on disk while appearing in neither place, and it then reaches no tool call. Prose that gives such a module a present-tense action describes a gate the reader never faces, and a reader who trusts it skips the check they were meant to run by hand.
9
+
10
+ The staged policy lint carries this check as its `retired-hook-prose` rule. It reads each Markdown file under a `rules/` directory, takes every backticked hook module name, and reports the ones that are registered nowhere and that carry a present-tense action verb in the rest of the sentence. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base.
11
+
12
+ A module name counts as a hook when it ends in one of the hook families, such as `_blocker`, `_enforcer`, `_gate`, `_tracker`, or `_dispatcher`, and when it either sits under `hooks/` or appears in `RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS` in `bin/install.mjs`. A support module inside the hooks tree carries no such family suffix, so it stays out of the check. A hook deleted from disk without an entry in that roster resolves nowhere, so add the roster entry when you retire a hook.
13
+
14
+ Two shapes pass. A past-tense sentence that records what a hook once did makes no live claim. A sentence that names the staged policy lint, `cde_lint`, or a repository check tells the reader where the work runs now, so the module name reads as a pointer to code.
15
+
16
+ When the rule reports a line, say what the reader faces now. Name the harness permission prompt, the staged policy lint, or the repository check that carries the work, or drop the claim.
@@ -18,4 +18,20 @@ When a script file's literal body needs `$(...)`, author it with the Write tool,
18
18
 
19
19
  ## Enforcement
20
20
 
21
- `shell_substitution_blocker.py` (PreToolUse on Bash, hosted by `bash_pre_tool_use_dispatcher`) denies a command carrying a live substitution and returns the split-into-two-calls rewrite. Single-quoted runs are stripped before the scan, and a backtick preceded by an odd number of backslashes is escaped, so an inert mention passes. A quoted heredoc body is dropped for the same reason: the opener forms that quote or escape the delimiter tell bash to expand nothing down to the terminator, so a backtick there is text the file receives. A bare delimiter expands its body and keeps its scan.
21
+ No PreToolUse hook denies a Bash command. Commit `0f21faf8e` retired the blocking policy hooks and left the Bash PreToolUse roster empty. `shell_substitution_blocker.py` was one of them. The substitution constraint above is guidance a reader follows, and a permission prompt on a wrapped command is the signal that one slipped through.
22
+
23
+ One PreToolUse hook does run on a Bash command, and it only rewrites. `blocking/msys_rev_path_rewriter.py` (PreToolUse on Bash, hosted by `bash_pre_tool_use_dispatcher`) reads a git command before it runs and keeps Git Bash from converting a `<rev>:<path>` argument. That roster holds this one hook, and a test asserts its whole content, so a blocking hook added beside it fails the suite.
24
+
25
+ Git Bash rewrites that argument when the revision holds a slash and the path after the colon starts with a slash or a dot. It turns the colon into a semicolon and the slashes into backslashes, so `git show origin/main:.claude/settings.json` reaches git as `origin\main;.claude\settings.json` and git reports a revision that does not exist. A leading dot on a file is enough. `origin/main:.gitignore` reaches git as `origin\main;.gitignore`. `git show origin/main:packages/app.py` passes through untouched, and so does any path after the colon that starts with `./`, `../`, or `~/`. A revision without a slash, such as `HEAD:.claude/settings.json`, passes through too.
26
+
27
+ On that shape the rewriter names only the arguments it found, so `git show origin/main:.claude/settings.json` runs as:
28
+
29
+ ```
30
+ export MSYS2_ARG_CONV_EXCL='origin/main:'; git show origin/main:.claude/settings.json
31
+ ```
32
+
33
+ `MSYS2_ARG_CONV_EXCL` takes a semicolon-separated list of argument prefixes. Naming one prefix per detected token leaves every other argument in the command converting as before. The blanket pair `MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'` turns conversion off for the whole command, which changes a path the command meant to convert, so the rewriter does not emit it.
34
+
35
+ A quoted token is not a revision. `git commit -m "fix a/b:.py"` carries a slash and a dot in one token, and a rewrite there would break the message, so the detection skips a token holding whitespace or a quote.
36
+
37
+ `advisory/msys_path_conversion_advisor.py` (PostToolUse on Bash, hosted by `bash_post_call_dispatcher`) stays behind it. It reads a failed git call, looks for the mark MSYS leaves, and names the fix. It covers a shape the rewriter misses.
package/rules/testing.md CHANGED
@@ -23,10 +23,8 @@ Incomplete mocks make it impossible to distinguish "broken code" from "missing d
23
23
 
24
24
  Tests exercise real behavior, real data, and production code paths. A test that asserts on a stand-in for the production path proves the stand-in works.
25
25
 
26
- ## The File-Level TDD Gate Reads Content
26
+ ## No Gate Holds the Test-First Order
27
27
 
28
- `tdd_enforcer.py` records each candidate test's content hash at every sighting and compares that hash on the next write. A first sighting requires content that differs from HEAD.
28
+ `tdd_enforcer.py` once required a fresh, failing test before a production module was written. Nothing runs it now, and the staged policy lint carries no replacement. Hold the red-green-refactor order yourself, and let review check it on the diff.
29
29
 
30
- ## The Gate Reads a Recorded Failing Run First
31
-
32
- `hooks/observability/test_failure_recorder.py` runs on every Bash call and records a single unchained pytest run that names a real test file path and reports a failing exit status. It stores the command, that exit status, and the path in the same content-hash store the gate reads. The gate consults that record before its freshness fallback, and honours it while the candidate's content still matches what failed. `content_hash_store.py`'s module docstring holds the full contract.
30
+ `hooks/observability/test_failure_recorder.py` still runs on every Bash call. It records a single unchained pytest run that names a test file path and reports a failing exit status, storing the command, that exit status, and the path in the content-hash store. `content_hash_store.py`'s module docstring holds the full contract. No gate reads that store today, so the record serves as history rather than a precondition.
@@ -22,7 +22,7 @@ Before stating a runtime claim, gather the matching live signal:
22
22
  | The config is in effect | Print the loaded config at runtime and read the value. |
23
23
  | The server is up | Probe the port; a refused connection means it is down. |
24
24
  | The process is running | List processes and match the name or PID. |
25
- | The change took effect | Drive the flow and watch the new behavior. |
25
+ | The change took effect | Drive the flow and watch the new behavior. A script that proves a branch's behavior names the module file it loaded, as its first step, because an installed copy of the same package shadows the checkout you meant to test. |
26
26
  | The dependency is reachable | Send one real request and read the response. |
27
27
  | The release or deploy shipped | Read what it makes: the tag, the published version, the file on disk. |
28
28
  | The pipeline did the work | Read each job's own result. A run reports success while a job inside it is skipped. |
@@ -12,6 +12,6 @@ Never call `shutil.rmtree` with `ignore_errors=True` — Windows `ReadOnly` file
12
12
 
13
13
  In Node, call `mkdirSync(targetPath, { recursive: true })` on possibly-existing paths — `ReadOnly` directories break the non-recursive form. When the call must be non-recursive, strip the attribute first (`(Get-Item $path -Force).Attributes = "Directory"` / `os.chmod(path, stat.S_IWRITE)`).
14
14
 
15
- The `windows_rmtree_blocker.py` PreToolUse hook (Write/Edit/Bash) blocks the unsafe rmtree pattern and returns the full `force_rmtree` safe-pattern code.
15
+ The staged policy lint carries this check as its `rmtree-safety` rule. It reports the unsafe rmtree pattern and returns the full `force_rmtree` safe-pattern code. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base.
16
16
 
17
- Define the safe handler trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`, and the `inspect.signature` onexc/onerror guard) once in a shared Windows-filesystem utility module, and import it from every call site. A second local copy drifts from the first — a fix lands in one and the other keeps the bug (CODE_RULES.md, CORE PRINCIPLES — "Reuse before create"). The `duplicate_rmtree_helper_blocker.py` PreToolUse hook (Write/Edit) blocks a local re-definition of any trio member outside the shared home and points the writer at the import. This complements the same-directory `check_duplicate_function_body_across_files` gate, which a copy between two distant packages slips past.
17
+ Define the safe handler trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`, and the `inspect.signature` onexc/onerror guard) once in a shared Windows-filesystem utility module, and import it from every call site. A second local copy drifts from the first, so a fix lands in one and the other keeps the bug (CODE_RULES.md, CORE PRINCIPLES, "Reuse before create"). The same `rmtree-safety` lint rule reports a local re-definition of any trio member outside the shared home and points the writer at the import. This complements the same-directory `check_duplicate_function_body_across_files` check, which a copy between two distant packages slips past.
@@ -9,4 +9,4 @@ In a `.workflow.js` agent-prompt template, every per-call or per-iteration value
9
9
 
10
10
  When a loop builds a per-iteration path or output key, write the index as a slot — `cand_<i>` — or spell out `replace <i> with the iteration index 0, 1, 2` in the step text. Every per-call value in a `.workflow.js` template carries angle brackets so an agent fills in a fresh value per call.
11
11
 
12
- `workflow_substitution_slot_blocker.py` (PreToolUse on Write/Edit) blocks a `.workflow.js` write whose looped content carries a bare `<word>_<i|j|k>` token as a per-iteration path segment, and returns the corrective message.
12
+ The staged policy lint carries this check as its `workflow-substitution` rule. It reports a `.workflow.js` file whose looped content holds a bare `<word>_<i|j|k>` token as a per-iteration path segment. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base. No write-time hook reports this, so a bare token stays on disk until the lint runs.
@@ -10,7 +10,14 @@ from .config.constants import (
10
10
  GIT_EXECUTABLE,
11
11
  )
12
12
  from .model import AdvisoryRegistration, LocalCheckout
13
- from .window_flags import hidden_window_creation_flags
13
+
14
+ import sys
15
+
16
+ _subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
17
+ if _subprocess_window_hooks_directory not in sys.path:
18
+ sys.path.append(_subprocess_window_hooks_directory)
19
+
20
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
14
21
 
15
22
 
16
23
  def read_local_checkout(checkout_path: Path) -> LocalCheckout:
@@ -38,7 +38,14 @@ from automatic_advisory.runner import (
38
38
  AdvisoryGitHub,
39
39
  AutomaticAdvisoryRunner,
40
40
  )
41
- from automatic_advisory.window_flags import detached_poller_creation_flags
41
+
42
+ _subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
43
+ if _subprocess_window_hooks_directory not in sys.path:
44
+ sys.path.append(_subprocess_window_hooks_directory)
45
+
46
+ from hooks_constants.subprocess_window import (
47
+ detached_hidden_window_creation_flags,
48
+ )
42
49
 
43
50
 
44
51
  def build_parser() -> argparse.ArgumentParser:
@@ -134,7 +141,6 @@ def start_polling(settings_path: Path) -> int:
134
141
  Returns:
135
142
  Zero after the child process starts.
136
143
  """
137
- process_flags = detached_poller_creation_flags()
138
144
  resolved_settings_path = settings_path.resolve()
139
145
  subprocess.Popen(
140
146
  (
@@ -148,7 +154,7 @@ def start_polling(settings_path: Path) -> int:
148
154
  stdin=subprocess.DEVNULL,
149
155
  stdout=subprocess.DEVNULL,
150
156
  stderr=subprocess.DEVNULL,
151
- creationflags=process_flags,
157
+ creationflags=detached_hidden_window_creation_flags(),
152
158
  start_new_session=True,
153
159
  )
154
160
  return SUCCESS_EXIT_CODE
@@ -75,8 +75,6 @@ WINDOWS_EXTENDED_LIMIT_INFORMATION_CLASS = 9
75
75
  WINDOWS_JOB_KILL_ON_CLOSE_LIMIT = 0x2000
76
76
  DEFAULT_CLONE_URL_SUFFIX = ".git"
77
77
  POLL_LOCK_DIRECTORY_NAME = ".poll-process"
78
- CREATE_NEW_PROCESS_GROUP_ATTRIBUTE = "CREATE_NEW_PROCESS_GROUP"
79
- CREATE_NO_WINDOW_ATTRIBUTE = "CREATE_NO_WINDOW"
80
78
  CHILD_MANIFEST_FLAG = "--manifest"
81
79
  CHILD_REPOSITORY_FLAG = "--repo"
82
80
  CHILD_BASE_FLAG = "--base"
@@ -26,7 +26,6 @@ from .config.constants import (
26
26
  )
27
27
  from .config.timing import CHILD_CLEANUP_TIMEOUT_SECONDS
28
28
  from .model import AdvisoryRegistration, ChildOutcome
29
- from .window_flags import hidden_window_creation_flags
30
29
  from .windows_job import (
31
30
  _assign_process,
32
31
  _close_handle,
@@ -34,6 +33,12 @@ from .windows_job import (
34
33
  _terminate_job,
35
34
  )
36
35
 
36
+ _subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
37
+ if _subprocess_window_hooks_directory not in sys.path:
38
+ sys.path.append(_subprocess_window_hooks_directory)
39
+
40
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
41
+
37
42
 
38
43
  @dataclass(frozen=True)
39
44
  class OwnedChildProcess:
@@ -165,6 +170,7 @@ def _start_posix_child(
165
170
  text=True,
166
171
  env=all_child_environment,
167
172
  start_new_session=True,
173
+ creationflags=hidden_window_creation_flags(),
168
174
  )
169
175
  except OSError:
170
176
  return None
@@ -17,7 +17,15 @@ from .config.constants import (
17
17
  )
18
18
  from .config.timing import GIT_FETCH_TIMEOUT_SECONDS
19
19
  from .model import AdvisoryRegistration
20
- from .window_flags import hidden_window_creation_flags
20
+
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ _subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
25
+ if _subprocess_window_hooks_directory not in sys.path:
26
+ sys.path.append(_subprocess_window_hooks_directory)
27
+
28
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
21
29
 
22
30
 
23
31
  def _fetch_base(
@@ -8,6 +8,8 @@ from pr_verification.config.constants import INCOMPLETE_EXIT_CODE
8
8
 
9
9
  from .config.constants import WINDOWS_PROCESS_START_SIGNAL
10
10
 
11
+ from subprocess_window_access import inherited_stream_startup_info
12
+
11
13
 
12
14
  def run_owned_child(all_arguments: Sequence[str]) -> int:
13
15
  """Wait for ownership, then run the advisory child.
@@ -24,6 +26,7 @@ def run_owned_child(all_arguments: Sequence[str]) -> int:
24
26
  tuple(all_arguments),
25
27
  shell=False,
26
28
  check=False,
29
+ startupinfo=inherited_stream_startup_info(),
27
30
  )
28
31
  return completed_process.returncode
29
32