claude-dev-env 2.0.1 → 2.1.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 (127) hide show
  1. package/CLAUDE.md +1 -1
  2. package/agents/clean-coder.md +31 -1
  3. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  4. package/bin/install.test.mjs +2 -2
  5. package/docs/CLAUDE.md +6 -3
  6. package/docs/CODE_RULES.md +5 -1
  7. package/docs/agent-spawn-protocol.md +39 -0
  8. package/docs/nas-ssh-invocation.md +23 -0
  9. package/docs/worker-completion-gate.md +33 -0
  10. package/hooks/blocking/CLAUDE.md +1 -0
  11. package/hooks/blocking/code_rules_dead_module_constant.py +11 -5
  12. package/hooks/blocking/code_rules_shared.py +126 -47
  13. package/hooks/blocking/config/CLAUDE.md +2 -0
  14. package/hooks/blocking/config/verified_commit_constants.py +8 -1
  15. package/hooks/blocking/config/verified_commit_context_constants.py +21 -0
  16. package/hooks/blocking/config/verified_commit_gate_output_constants.py +14 -0
  17. package/hooks/blocking/conftest.py +36 -30
  18. package/hooks/blocking/convergence_gate_blocker.py +76 -8
  19. package/hooks/blocking/plain_language_blocker.py +8 -0
  20. package/hooks/blocking/state_description_blocker.py +4 -1
  21. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +58 -0
  22. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +133 -0
  23. package/hooks/blocking/test_code_rules_shared.py +120 -20
  24. package/hooks/blocking/test_convergence_gate_blocker.py +146 -0
  25. package/hooks/blocking/test_plain_language_blocker.py +15 -0
  26. package/hooks/blocking/test_state_description_blocker.py +15 -0
  27. package/hooks/blocking/test_verification_verdict_store.py +12 -0
  28. package/hooks/blocking/test_verified_commit_config_bootstrap.py +18 -0
  29. package/hooks/blocking/test_verified_commit_docs_delta.py +176 -0
  30. package/hooks/blocking/test_verified_commit_gate_additional_context.py +134 -0
  31. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -1
  32. package/hooks/blocking/tests/test_verified_commit_gate.py +41 -0
  33. package/hooks/blocking/verified_commit_config_bootstrap.py +22 -10
  34. package/hooks/blocking/verified_commit_gate.py +113 -568
  35. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +28 -0
  36. package/hooks/blocking/verified_commit_gate_parts/__init__.py +1 -0
  37. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +174 -0
  38. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +53 -0
  39. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +80 -0
  40. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +170 -0
  41. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +205 -0
  42. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +10 -0
  43. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +94 -0
  44. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +17 -0
  45. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +38 -0
  46. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +71 -0
  47. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +61 -0
  48. package/hooks/hooks_constants/CLAUDE.md +4 -1
  49. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  50. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +36 -0
  51. package/hooks/hooks_constants/dead_module_constant_constants.py +8 -0
  52. package/hooks/hooks_constants/harness_scratchpad_constants.py +10 -9
  53. package/hooks/hooks_constants/mypy_integration_constants.py +16 -0
  54. package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
  55. package/hooks/validators/mypy_integration.py +145 -24
  56. package/hooks/validators/python_antipattern_checks.py +16 -0
  57. package/hooks/validators/run_all_validators.py +9 -3
  58. package/hooks/validators/test_mypy_integration.py +154 -0
  59. package/hooks/validators/test_python_antipattern_checks.py +112 -1
  60. package/hooks/validators/test_run_all_validators_pretooluse.py +16 -0
  61. package/package.json +1 -1
  62. package/rules/CLAUDE.md +3 -3
  63. package/rules/agent-spawn-protocol.md +5 -43
  64. package/rules/code-standards.md +1 -36
  65. package/rules/env-var-table-code-drift.md +2 -21
  66. package/rules/hook-prose-matches-detector.md +5 -16
  67. package/rules/nas-ssh-invocation.md +3 -15
  68. package/rules/no-historical-clutter.md +7 -49
  69. package/rules/no-inline-destructive-literals.md +3 -5
  70. package/rules/package-inventory-stale-entry.md +7 -32
  71. package/rules/re-stage-before-commit.md +6 -23
  72. package/rules/shell-invocation-policy.md +1 -1
  73. package/rules/vault-context.md +3 -3
  74. package/rules/verified-commit-gate-skip.md +28 -0
  75. package/rules/workers-done-before-complete.md +2 -30
  76. package/scripts/claude_chain_runner.py +39 -3
  77. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +3 -0
  78. package/scripts/test_claude_chain_runner.py +112 -0
  79. package/scripts/test_grok_headless_runner.py +0 -1
  80. package/skills/auditing-claude-config/CLAUDE.md +2 -1
  81. package/skills/auditing-claude-config/SKILL.md +114 -176
  82. package/skills/auditing-claude-config/reference/probe-hook.md +74 -0
  83. package/skills/autoconverge/CLAUDE.md +1 -0
  84. package/skills/autoconverge/SKILL.md +310 -346
  85. package/skills/autoconverge/reference/CLAUDE.md +1 -0
  86. package/skills/autoconverge/reference/copilot-findings.md +51 -0
  87. package/skills/closeout/SKILL.md +7 -9
  88. package/skills/copilot-finding-triage/SKILL.md +5 -7
  89. package/skills/copilot-review/CLAUDE.md +3 -2
  90. package/skills/copilot-review/SKILL.md +119 -155
  91. package/skills/copilot-review/templates/subagent-prompt.md +49 -0
  92. package/skills/everything-search/SKILL.md +7 -1
  93. package/skills/fresh-branch/CLAUDE.md +6 -9
  94. package/skills/fresh-branch/SKILL.md +84 -33
  95. package/skills/fresh-branch/scripts/create_fresh_branch.py +445 -0
  96. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/__init__.py +1 -0
  97. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +74 -0
  98. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +670 -0
  99. package/skills/orchestrator/SKILL.md +161 -147
  100. package/skills/pr-converge/SKILL.md +277 -422
  101. package/skills/pr-converge/reference/CLAUDE.md +1 -0
  102. package/skills/pr-converge/reference/progress-checklist.md +168 -0
  103. package/skills/pr-converge/test_step5_host_branch.py +8 -6
  104. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  105. package/skills/privacy-hygiene/SKILL.md +68 -115
  106. package/skills/privacy-hygiene/reference/sweep-procedure.md +62 -0
  107. package/skills/session-log/CLAUDE.md +2 -1
  108. package/skills/session-log/SKILL.md +4 -26
  109. package/skills/session-log/templates/frontmatter.md +40 -0
  110. package/skills/skill-builder/CLAUDE.md +8 -7
  111. package/skills/skill-builder/SKILL.md +26 -11
  112. package/skills/skill-builder/references/CLAUDE.md +3 -1
  113. package/skills/skill-builder/references/delegation-map.md +21 -12
  114. package/skills/skill-builder/references/description-field.md +9 -11
  115. package/skills/skill-builder/references/deterministic-elements.md +218 -0
  116. package/skills/skill-builder/references/self-audit-checklist.md +62 -45
  117. package/skills/skill-builder/references/skill-modularity.md +8 -9
  118. package/skills/skill-builder/templates/CLAUDE.md +2 -2
  119. package/skills/skill-builder/templates/gap-analysis.md +15 -0
  120. package/skills/skill-builder/workflows/CLAUDE.md +5 -5
  121. package/skills/skill-builder/workflows/improve-skill.md +18 -9
  122. package/skills/skill-builder/workflows/new-skill.md +23 -15
  123. package/skills/skill-builder/workflows/polish-skill.md +28 -21
  124. package/system-prompts/CLAUDE.md +3 -3
  125. package/docs/agents-md-alignment-plan.md +0 -123
  126. package/docs/emotion-informed-prompt-design.md +0 -362
  127. package/rules/es-exe-file-search.md +0 -17
@@ -1,47 +1,9 @@
1
1
  # Agent Spawn Protocol (Mandatory)
2
2
 
3
- **When this applies:** Before any Agent or Task tool invocation (Explore, implementation, research, or team subagents).
3
+ Before any Agent or Task tool spawn (Explore, implementation, research, or team subagents):
4
4
 
5
- <agent_spawn_protocol>
5
+ 1. **Check context sufficiency** — you can name the files involved, the constraints, and what success looks like, and the task is unambiguous. When you cannot, investigate or ask the user first; do not spawn with incomplete context.
6
+ 2. **Craft the prompt with `/prompt-generator`** — feed it the goal, the target files from step 1, the constraints, the output format, and the acceptance criteria; use its output as the agent's `prompt`.
7
+ 3. **Spawn** with that structured prompt.
6
8
 
7
- ## Before spawning ANY agent no exceptions
8
-
9
- Every Agent and Task tool call must follow this protocol. This includes Explore agents, research agents, execution agents, and team members.
10
-
11
- ### Step 1: Context sufficiency check
12
-
13
- Before writing any agent prompt, verify you can answer all of these:
14
- - [ ] What specific files, directories, or areas of the codebase are involved?
15
- - [ ] What constraints apply? (patterns to follow, things NOT to change, boundaries)
16
- - [ ] What does success look like? (expected output, acceptance criteria)
17
- - [ ] Is the task unambiguous enough to delegate?
18
-
19
- If ANY answer is "I don't know" -- investigate first (read files, search code) or ask the user. Do NOT spawn with incomplete context.
20
-
21
- ### Step 2: Craft the prompt with /prompt-generator
22
-
23
- Run the `/prompt-generator` skill to produce a structured prompt. Feed it:
24
- - The task description and goal
25
- - Target files/directories discovered in Step 1
26
- - Constraints and boundaries
27
- - Expected output format
28
- - Acceptance criteria
29
-
30
- The skill will ask 1-3 clarifying questions if information is missing -- this is the built-in context verification.
31
-
32
- Use the skill's output as the agent's `prompt` parameter.
33
-
34
- ### Step 3: Spawn the agent
35
-
36
- Pass the structured prompt from Step 2 to the Agent/Task tool.
37
-
38
- </agent_spawn_protocol>
39
-
40
- ## Why
41
-
42
- Agents receiving vague prompts waste tokens exploring in circles, produce code that misses constraints, and require expensive rework. A 30-second investment in prompt quality via /prompt-generator saves 5-minute agent failures. This applies equally to Explore agents (which waste context on unfocused searches) and execution agents (which write wrong code).
43
-
44
- ## Relationship to other rules
45
-
46
- - **conservative-action.md** gates acting when ambiguous. This extends that: do not delegate when the task is ambiguous—investigate or ask the user first.
47
- - Project-specific rules or `~/.claude/CLAUDE.md` may define *whether* to use subagents or teams; this rule governs *how* to craft prompts when you do delegate.
9
+ Full step detail, rationale, and relationship to other rules: `@~/.claude/docs/agent-spawn-protocol.md`.
@@ -3,39 +3,4 @@
3
3
  > **MANDATORY REFERENCE:** CODE_RULES.md - Load for ALL code generation.
4
4
  > This is the single source of truth for code standards. Non-negotiable.
5
5
 
6
- **Key principles (see CODE_RULES.md for complete reference):**
7
- - Self-documenting code (no comments)
8
- - Centralized configuration (one source of truth)
9
- - Reuse constants (search before creating)
10
- - No magic values (everything named)
11
- - No abbreviations (full words)
12
- - Complete type hints
13
- - TDD (test first)
14
-
15
- ## Function Parameters - Required vs Optional
16
-
17
- **Use required parameters when no valid use case exists for optional.**
18
- **Remove unused parameters.**
19
-
20
- ## Encapsulation - Logic Belongs in Models
21
-
22
- **NEVER scatter construction logic in calling code.**
23
-
24
- Path/URL building, formatting, transformations -> Put in model methods.
25
- If you find yourself building the same string pattern in multiple places, it belongs in the model.
26
-
27
- ## Document Temporary Code
28
-
29
- **Scaffolding/placeholder code MUST have TODO comments.**
30
-
31
- When code exists only to enable testing before full implementation:
32
- - Add `// TODO: Replace with...` explaining what will replace it
33
- - Explain WHY it's temporary, not just WHAT it does
34
-
35
- ## Naming Reflects Behavior
36
-
37
- **Name components after what they ARE, not abstract concepts.**
38
-
39
- If it overlays the viewport -> "Overlay" not "Screen"
40
- If it validates input -> "Validator" not "Handler"
41
- Names should describe observable behavior or visual appearance.
6
+ `CODE_RULES.md` (`~/.claude/docs/CODE_RULES.md`) is the compact reference for every standard: self-documenting names, centralized configuration, constant reuse, no magic literals, full words, complete type hints, required-vs-optional parameters, construction logic in the model, temporary-code `TODO:` markers, behavior-first component names, and TDD.
@@ -5,25 +5,6 @@ paths:
5
5
 
6
6
  # Env-Var Summary Table Names a Code File That Reads the Variable
7
7
 
8
- **When this applies:** Any Write, Edit, or MultiEdit to a markdown (`.md`) file that carries an environment-variable summary table a markdown table whose rows pair a `` `VARIABLE` `` name with the `` `code/file.py` `` that reads it.
8
+ 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 table row that names that file.
9
9
 
10
- ## Rule
11
-
12
- Every row in an env-var summary table names a code file whose source references the variable. A row pairs an UPPER_SNAKE variable with a code-file path, written as `` | `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | ... | ``, and the named file reads that variable. When the file exists yet its source never mentions the variable name, the row is stale: the table points a reader at a consumer relationship the code does not have, so a reader trusts the doc to behavior the code dropped.
13
-
14
- When a code change removes the last read of a variable from a file, the same change drops or corrects the table row that names that file. The doc and the code move together in one commit.
15
-
16
- ## What the gate checks
17
-
18
- The `env_var_table_code_drift_blocker.py` hook runs on every Write, Edit, and MultiEdit whose target is a `.md` file. It:
19
-
20
- 1. Reads the content the tool would leave on disk, skipping lines inside a fenced code block.
21
- 2. Collects each table row whose first cell names an UPPER_SNAKE variable and whose later cell names a code file with a recognized extension (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`).
22
- 3. Resolves the named code file under the repository root (the nearest `.git`-bearing ancestor of the markdown file) and reads its source.
23
- 4. Blocks the write when the file resolves yet its source never references the variable name. For an Edit, drift the file already held on an untouched row is excluded, so only drift the edit introduces is reported.
24
-
25
- The check stays quiet for a row whose code file resolves nowhere under the repository root (it cannot prove the drift), a row whose second cell holds no code-file path, and a table row inside a fenced code block.
26
-
27
- ## Why this is a hook, not a lint pass
28
-
29
- An env-var table that names a file whose source skips the variable reads as a correct map of which code consumes which setting, while pointing one row at behavior the code dropped. A reader trusting the row chases a setting the file ignores, and the gap survives review because the table still looks complete. Catching it as the doc is written keeps the table and the code in step.
10
+ `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 (the hook cannot prove the drift).
@@ -4,23 +4,12 @@ paths: **/hooks/**/*.py
4
4
 
5
5
  # Hook Prose Matches Its Detector
6
6
 
7
- **When this applies:** Any Write or Edit to a hook module (`.py` under `hooks/`) or its `*_constants.py` companion.
7
+ A hook's docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags no broader trigger surface than the regex enforces.
8
8
 
9
- **Hook enforcement:** `hook-prose-detector-consistency` (PreToolUse on Write|Edit) blocks a hook whose user-facing prose claims a trigger its detector never fires on. See `hooks.json` for registration.
9
+ `hook_prose_detector_consistency` (PreToolUse on Write|Edit of hook modules and `*_constants.py` companions) blocks prose that claims a trigger the detector never fires on, and names the fix.
10
10
 
11
- ## Rule
11
+ ## Judgment
12
12
 
13
- A hook's docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags no broader trigger surface than the regex enforces. An author reads the corrective message to learn what they did wrong; an author reads the docstring to learn what the hook guards. When either claims a trigger the detector cannot fire on, both audiences are misled: an author whose only token is that shape never sees the block, and an author who does see the block is told the wrong cause.
13
+ After writing a hook, ask: would a token that matches 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.
14
14
 
15
- ## The path-shape blocker case
16
-
17
- A path-shape blocker detects a per-iteration token only when the token sits next to a path separator (its detection regex keys off a `[\\/]`-style character class). Such a hook must not claim it blocks an "output-key segment": a quoted structured-output key alone, with no looped path, is never flagged. The `*_constants.py` companion holds the corrective message and not the detector, so the phrase "output-key segment" describing a blocked trigger is itself the violation there, regardless of which file holds the regex.
18
-
19
- | Prohibited claim | Why it overstates | Correct phrasing |
20
- |---|---|---|
21
- | "appears as a path or output-key segment" | the detector keys off a path separator only | "appears as a per-iteration path segment" |
22
- | docstring: "blocks a bare token like `cand_i`" | a bare prose token next to no separator is not flagged | "blocks a per-iteration path like `${work}\cand_i\plate.svg`" |
23
-
24
- ## The test
25
-
26
- After writing a hook, ask: **would a token that matches 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.
15
+ 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.
@@ -1,23 +1,11 @@
1
1
  # NAS SSH Invocation Policy
2
2
 
3
- **When this applies:** Any `ssh`, `scp`, or `sftp` command against the NAS.
4
-
5
- ## Rule
6
-
7
- Reach the NAS through the Windows OpenSSH binary with batch mode on. 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
- Use this form for every NAS ssh command:
3
+ Reach the NAS through the Windows `System32/OpenSSH` binary with `-o BatchMode=yes` on every `ssh`, `scp`, or `sftp` command:
10
4
 
11
5
  ```
12
6
  "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 -p 22 operator@nas.example.local "<cmd>"
13
7
  ```
14
8
 
15
- `scp` and `sftp` take the matching `System32/OpenSSH` binary, `-o BatchMode=yes`, and the same port (`-P` for `scp`).
16
-
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`).
18
-
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.
20
-
21
- ## Enforcement
9
+ Git Bash's MSYS `ssh` falls back to an interactive password prompt that hangs an unattended session; the `System32/OpenSSH` binary authenticates the key without a prompt, and `-o BatchMode=yes` turns an auth failure into a loud non-zero exit. `nas_ssh_binary_enforcer.py` (PreToolUse on Bash) enforces this: it denies a bare ssh-family word aimed at the NAS, and denies the full binary when `-o BatchMode=yes` is absent.
22
10
 
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.
11
+ Host, user, and port config, the `scp`/`sftp` forms, and the full rationale: `@~/.claude/docs/nas-ssh-invocation.md`.
@@ -11,58 +11,16 @@ paths:
11
11
 
12
12
  # No Historical Clutter in Documentation or Comments
13
13
 
14
- **When this applies:** Any Write or Edit to files containing comments or documentation.
14
+ Never reference removed implementations, old defaults, prior behaviors, or earlier contracts when updating documentation or comments. The current state is all that matters. A module or function docstring carries the same describe-current-state-only contract as a `.md` file.
15
15
 
16
- **Hook enforcement:** `state-description-blocker` (PreToolUse on Write|Edit) blocks historical/comparative language automatically. See `hooks.json` for registration.
16
+ `state_description_blocker` (PreToolUse on Write|Edit) blocks historical and comparative phrases in `.md` prose, code comments, and Python docstrings; a phrase wrapped in double quotes or backticks inside a docstring counts as a mention and is skipped. The denial names the matched phrases and shows a rewrite example.
17
17
 
18
- Coverage spans `.md` prose, code comments, and Python module/class/function docstrings; a phrase wrapped in double quotes or backticks inside a docstring counts as a mention and is skipped.
18
+ ## What stays allowed
19
19
 
20
- ## Rule
21
-
22
- Never reference removed implementations, old defaults, prior behaviors, or how something `"used to be"` when updating documentation. The current state is all that matters.
23
-
24
- A module or function docstring carries the same describe-current-state-only contract as a `.md` file.
25
-
26
- ## Examples of prohibited patterns
27
-
28
- ### In documentation (.md files)
29
-
30
- | Pattern | Why it's clutter |
31
- |---------|-----------------|
32
- | `` `"instead of 30"` `` in a pagination rule | The old default `no longer` exists in code; the rule reader doesn't need to know what it was |
33
- | `` `"previously this used X"` `` | If X is gone, it's noise |
34
- | `` `"before this rule, we did Y"` `` | The rule exists now; the before-state is irrelevant |
35
- | `` `"migrated from Z to W"` `` | If Z is fully removed, the migration story is git history, not documentation |
36
- | `` `"the old implementation did A"` `` | If A is gone, the reader gains nothing from knowing it existed |
37
- | `` `"originally"` `` / `` `"used to be"` `` | Same — dead context |
38
-
39
- ### In code comments
40
-
41
- | Pattern | Good replacement |
42
- |---------|-----------------|
43
- | `# Uses X instead of Y` | `# Uses X` |
44
- | `# Previously configured via Z` | `# Configured via Z` |
45
- | `# Now uses the new API client` | `# Uses the new API client` |
46
- | `# No longer supports legacy mode` | `# Supports modern mode only` |
47
- | `// Switched to async processing` | `// Processes asynchronously` |
48
- | `# Replaced by the cache layer` | `# Cache layer handles reads` |
49
-
50
- ### Hook-detected patterns
51
-
52
- The `state-description-blocker` hook (PreToolUse on Write\|Edit) enforces these patterns automatically:
53
-
54
- `instead of`, `previously`, `now uses/does/handles/supports/names/includes`, `was previously`, `were previously`, `was formerly`, `was added`, `used to`, `no longer`, `has/have been updated/changed`, `replaced by`, `replaces`, `superseded by`, `supersedes`, `changed from`, `changes from`, `switched from/to`, `migrated from/to`, `moved to/into`, `extracted as`, `updated to`, `originally`, `as of`
55
-
56
- ## What IS allowed
57
-
58
- - Comparisons to *currently existing* alternatives (e.g., "use `--paginate --slurp | jq`, not `--jq` alone")
59
- - Rationale that explains *why* a pattern is wrong in terms of present behavior (e.g., "`--jq` runs per-page, so cross-page operations produce wrong results")
60
- - References to external sources for defects that still exist (e.g., gh CLI #10459)
20
+ - Comparisons to alternatives that still exist (for example, "use `--paginate --slurp | jq`, not `--jq` alone")
21
+ - Rationale that explains why a pattern is wrong in terms of present behavior (for example, "`--jq` runs per-page, so cross-page operations produce wrong results")
22
+ - References to external sources for defects that still exist (for example, gh CLI #10459)
61
23
 
62
24
  ## The test
63
25
 
64
- After writing documentation, ask: **"If someone reads this a year from now, with no knowledge of what came before, does every sentence still make sense and add value?"** If a sentence only adds value to someone who knew the old state, delete it.
65
-
66
- ## Why
67
-
68
- Historical references clog context windows and force readers to mentally filter "what was" from "what is." The git log is the authoritative record of what changed and why. Documentation describes the current contract.
26
+ After writing, ask: if someone reads this a year from now with no knowledge of earlier states, does every sentence still make sense and add value? If a sentence only helps someone who knew an earlier state, delete it.
@@ -1,11 +1,9 @@
1
1
  # No Inline Destructive-Command Literals in Bash
2
2
 
3
- The `destructive_command_blocker` PreToolUse hook matches destructive patterns (`rm -rf`, `git reset --hard`, `dd`, `mkfs`, `chmod -R`, fork bombs) as raw text anywhere in a Bash-tool command, with no quote-awareness — so a destructive literal carried only as DATA (a commit message, a PR/issue/review-comment body, an echoed string, a `python -c`/`node -e`/`awk` argument, a heredoc) trips the confirmation prompt even though the shell never executes it. In a background or auto-mode run no human can answer that prompt, so the call stalls.
3
+ The `destructive_command_blocker` PreToolUse hook matches destructive patterns (`rm -rf`, `git reset --hard`, `dd`, `mkfs`, `chmod -R`, fork bombs) as raw text anywhere in a Bash-tool command, with no quote-awareness — so a destructive literal carried only as data (a commit message, a PR/issue body, an echoed string, a `python -c` / `node -e` / `awk` argument, a heredoc) trips the confirmation prompt even though the shell never executes it. In a background or auto-mode run no human can answer that prompt, so the call stalls.
4
4
 
5
5
  Keep destructive literals out of the Bash command string:
6
6
 
7
- - Commit messages and PR/issue/review-comment bodies that describe destructive-command behavior go in a file passed by path — `git commit -F <file>`, `gh ... --body-file <file>` (see [`gh-body-file`](gh-body-file.md)) — never `git commit -m` / `gh ... -b`.
8
- - To exercise or verify `destructive_command_blocker` (or any hook) behavior, run the committed test suite (`python -m pytest <test_file>`), which passes the command strings as in-language data, not as a shell command — never an inline `python -c` harness.
7
+ - Bodies that describe destructive-command behavior go in a file passed by path — `git commit -F <file>`, `gh ... --body-file <file>` (see [`gh-body-file`](gh-body-file.md)) — never `git commit -m` / `gh ... -b`.
8
+ - 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.
9
9
  - Genuine cleanup targets the OS temp dir or `$CLAUDE_JOB_DIR/tmp` (auto-allowed as ephemeral), never a repository or worktree path.
10
-
11
- The `destructive_command_blocker` hook is the enforcement surface; this rule is how to keep a non-executing mention from tripping it.
@@ -10,41 +10,16 @@ paths:
10
10
 
11
11
  # New Production File Absent From Its Package Inventory
12
12
 
13
- **When this applies:** Any Write that creates a new production code file (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`) in a directory whose sibling `README.md` or `CLAUDE.md` already names two or more of the directory's files in backticks, in a directory whose own `SKILL.md` names two or more of the directory's files, or in a skill's `scripts/` subdirectory whose parent `SKILL.md` Layout table already names two or more of those scripts.
13
+ 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. When you create a new production file in such a directory, add an entry naming it a row in the table or a bullet in the list in the same change. The entry names the file in backticks and says what it does.
14
14
 
15
- ## Rule
15
+ `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.
16
16
 
17
- A package directory that documents its own files in a `README.md` Layout table or a `CLAUDE.md` "Key files" list keeps that inventory in step with the directory. A skill package does the same in its `SKILL.md` Layout table, which sits at the skill root and maps the `scripts/` subdirectory by naming each script with its `scripts/<name>` path. A new production file the inventory does not name leaves the inventory and the directory disagreeing on the package's file set: a reader who trusts the inventory to map the directory misses the new file.
17
+ ## Judgment the gate cannot derive
18
18
 
19
- When you create a new production file in such a directory, add an entry naming it a row in the `README.md` or `SKILL.md` table, a bullet in the `CLAUDE.md` list — in the same change. The entry names the file in backticks and says what it does.
19
+ The file-list entry is the slice the gate checks by name. Two free-prose slices stay with judgment and belong in the same change:
20
20
 
21
- ## Companion: keep the Purpose/scope sentence in step with the new responsibility
21
+ 1. **Purpose / scope sentence.** When the new module adds a responsibility the package `## Purpose` (or the parent inventory's one-line summary of this subdirectory) omits, broaden that sentence to name it. A hook cannot derive a module's responsibility from its filename.
22
22
 
23
- The file-list entry is the deterministic slice the gate enforces. A package inventory also carries a free-prose scope sentence a `## Purpose` paragraph in a `CLAUDE.md`, a one-line summary the parent directory's inventory gives each subdirectory that names the responsibilities the package's modules cover. When the new module adds a responsibility the scope sentence omits, the same change broadens that sentence to name it, and updates the parent inventory's one-line summary of this subdirectory to match.
23
+ 2. **Per-file description clause.** When a file gains a responsibility the inventory's em-dash description omits a new public function, a new module-level constantbroaden the description clause to name it. The gate only checks that the basename appears once; it never reads the description. Constants modules (`*_constants.py`, or any `.py` directly inside `config/`) are the common shape: the constant's other home is the module docstring, so the clause that lands in the docstring lands in the inventory description in the same change. The gate fires on Write of a new file and skips files directly inside `config/`, so an Edit that adds a constant to an existing config module matches neither path.
24
24
 
25
- Take a `files/` package whose `Purpose` reads "Holds helpers for downloading files over HTTP and extracting zip archives" and whose parent summary reads "file download, extraction, and path config helpers". Once a `force_remove.py` module that removes a directory tree sits beside the download helpers, both sentences name a narrower responsibility set than the directory holds. The required file-list bullet alone leaves that gap open. Broaden the `Purpose` sentence to name directory removal, and broaden the parent summary to match, in the same change that adds the module and its bullet.
26
-
27
- This scope-sentence slice is free prose: a hook cannot derive a module's responsibility from its filename, so the gate leaves it to judgment. It is the judgment companion to the file-list entry the gate enforces, and it belongs in the same change. This is the `category-o-docstring-vs-impl-drift` (O8) orphaned-doc-claim shape applied to a package inventory: a behavior change orphans a scope claim the prose still makes.
28
-
29
- ## Companion: keep a per-file description in step with the file it describes
30
-
31
- The per-file entry a `CLAUDE.md` "Key files" list or a `README.md` Layout table gives each file carries more than the backticked filename the gate checks for. The clause after the file name — the em-dash description — is itself a free-prose scope claim about what the file holds. When the file gains a responsibility the description omits — a new public function, a new constant — the same change broadens the description clause to name it. The gate's file-list check passes the moment the file name appears once; it never reads the description clause, so a stale description beside a present file name stays invisible to the gate.
32
-
33
- A constants module is the common shape of this drift. A file whose name ends `_constants.py`, or any `.py` directly inside a `config/` directory, holds a set of module-level constants, and a sibling inventory describes that file by listing the set — `` `stp_constants.py` — the STP archive member constants: the Properties.xml member name, the workspace prefix every asset reference carries, and the source-form nine-patch filename suffix ``. When the file gains a module-level constant the list omits, three claims drift together: the list itself, the scope label that heads it (`the STP archive member constants`), and the package `## Purpose` sentence when that sentence describes the file's contents. The constant's other home — the module docstring of the constants file — and the sibling inventory's description of that file cover the same set, so the clause that lands in the docstring lands in the inventory description in the same change.
34
-
35
- This slice sits outside the gate. The gate fires on a Write that creates a new file, and it skips a file directly inside a `config/` directory, so an Edit that adds a constant to an existing `config/` constants module matches neither path. Like the Purpose/scope companion above, it is free prose a hook cannot derive from a file name, so it stays judgment here and a Category O8 finding at audit: a behavior change orphans a description claim the inventory still makes.
36
-
37
- ## What the gate checks
38
-
39
- The `package_inventory_stale_blocker.py` hook runs on every Write whose target is a new file (a path not yet on disk). It:
40
-
41
- 1. Skips a target that is not a production code file (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`), an exempt basename (`__init__.py`, `conftest.py`, `setup.py`, `_path_setup.py`), a test file (`test_*.py`, `*_test.py`, `*.spec.*`, `*.test.*`), or a file directly inside a `config/` or `tests/` directory.
42
- 2. Reads each `README.md`, `CLAUDE.md`, and `SKILL.md` present in the target's own directory and, when the target sits in a `scripts/` subdirectory, the parent directory's `SKILL.md`, and collects every bare filename they name in backticks. A backticked token holding a path contributes its final segment, so `pipeline/seam_continuity.py` in an inventory counts as naming `seam_continuity.py` and `scripts/stp_selection.py` in a parent `SKILL.md` Layout table counts as naming `stp_selection.py`. A multi-word command-example span — one carrying whitespace or shell punctuation (`:`, `$`, `<`, `>`), such as `parent:node_modules package.json` or `python <file>.py` — names no literal file and is dropped.
43
- 3. Filters the named basenames to those that exist as a file in the target's own directory — the inventory's own sibling files — and treats the directory as carrying a maintained inventory only when two or more such sibling files are named. A directory with no inventory, one whose `README.md` mentions a single file in passing, or one whose inventory prose names only files living in other directories (so no named basename is an on-disk sibling) is out of scope.
44
- 4. Blocks the write when the new file's basename appears in no present inventory. An unreadable or oversized inventory document is skipped, so a missing inventory never blocks a write.
45
-
46
- The check fires on Write only — editing an existing file adds no new inventory entry — and stays quiet for a directory with no inventory document, an inventory naming too few siblings to be a maintained list, an exempt or test file, and a file the inventory already names.
47
-
48
- ## Why this is a hook, not a lint pass
49
-
50
- A package inventory that omits a file reads as a complete map of the directory while leaving one file off it. A reader trusting the inventory to list the package misses the new file, and the gap survives review because the inventory still looks complete. Catching it as the new file is written keeps the inventory and the directory in step. This is the counterpart to `claude-md-orphan-file.md`, which catches the reverse drift: an inventory entry naming a file the directory does not hold.
25
+ This is the `category-o-docstring-vs-impl-drift` (O8) orphaned-doc-claim shape applied to a package inventory.
@@ -1,31 +1,14 @@
1
1
  # Re-Stage Session Edits Before Commit
2
2
 
3
- **When this applies:** Any `git commit` run through the Bash tool in a git repository.
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
- ## Rule
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.
6
6
 
7
- Stage the files you edited this session right before you commit them. A file this session changed but left unstaged is dropped by a plain `git commit` — the commit records the staged snapshot and the edit stays behind in the working tree.
7
+ ## Escapes the denial does not restate
8
8
 
9
- The `session_edit_stage_gate` hook holds you to this. It reads the per-session tracker that records every file the session edited, checks which of those files are tracked yet still unstaged at commit time, and denies the commit when any are left out. The denial names each file and gives the exact fix: `git add <paths>`, `git commit -a`, or a `# partial-commit` marker.
10
-
11
- ## What the gate allows
12
-
13
- The gate steps aside for a commit that skips staged files on purpose:
14
-
15
- - **`-a` / `--all`** — the commit already takes every tracked change, so nothing is dropped.
16
- - **A pathspec** — `git commit -- <paths>` or `git commit <paths>` commits only the named paths on purpose.
17
- - **A preceding `git add` / `git stage`** — `git add <paths> && git commit …` stages the files in its own segment before the commit runs, so they are staged by the time the commit records the index.
18
- - **`# partial-commit`** — add this marker to the command to commit the staged set on purpose and leave the rest.
9
+ - **A pathspec** `git commit -- <paths>` or `git commit <paths>` commits only the named paths on purpose and steps the gate aside.
10
+ - **A preceding `git add` / `git stage`** — `git add <paths> && git commit …` stages the files in its own segment before the commit runs.
19
11
 
20
12
  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.
21
13
 
22
- A missing tracker file or any git failure allows the commit, so the gate never blocks on a tooling problem.
23
-
24
- ## Companion hooks
25
-
26
- - `session_file_edit_tracker` (PostToolUse) records the resolved absolute path of each Write, Edit, and MultiEdit into the per-session tracker file.
27
- - `session_edit_tracker_cleanup` (SessionStart, SessionEnd) deletes the running session's own tracker file: at a fresh SessionStart so a new session begins with an empty record, and at SessionEnd so a clean exit leaves nothing behind. A tracker is read only by the session that wrote it, so a file a crashed session leaves behind is inert and no peer session touches it.
28
-
29
- ## Why
30
-
31
- A stale git index is a quiet failure: the commit succeeds, the branch looks right, and one file you meant to include never lands. Catching it at commit time keeps the staged set and the session's edits in step.
14
+ `session_file_edit_tracker` (PostToolUse) records each Write/Edit/MultiEdit path; `session_edit_tracker_cleanup` (SessionStart, SessionEnd) clears the session's tracker.
@@ -2,4 +2,4 @@
2
2
 
3
3
  Every Bash-tool shell command on Windows uses `pwsh`: `pwsh -NoProfile -File '<script>.ps1' <args>` for scripts, `pwsh -NoProfile -Command "..."` (or a literal `@'...'@` here-string) for inline work, or the built-in `PowerShell` tool for pure-PowerShell workflows (it supports `run_in_background`). Never wrap a script path in `-Command "& '...'"` — `-File` keeps `permissions.allow` matching. The `&` call operator is fine for invoking an executable at a path (`& '<venv>\Scripts\python.exe' script.py`).
4
4
 
5
- `powershell`, `powershell.exe`, `cmd /c`, and `bash -c` are blocked by `permissions.deny` and the `pwsh_enforcer.py` PreToolUse hook, which returns the corrective pattern. Audit and migration scripts (`Audit-ShellPolicy.ps1`, `Migrate-ShellPolicy.ps1`) live in `packages/claude-dev-env/scripts/`.
5
+ Keep `powershell`, `powershell.exe`, `cmd /c`, and `bash -c` out of the `settings.json` permission rules. `Audit-ShellPolicy.ps1` reports those forms and `Migrate-ShellPolicy.ps1` rewrites them to `pwsh`, both in `packages/claude-dev-env/scripts/` and run on demand, not as a live gate.
@@ -1,7 +1,7 @@
1
1
  # Obsidian Vault Context
2
2
 
3
- An Obsidian vault stores session reports (`sessions/`), decisions (`decisions/`), and research (`Research/`) across projects. Resolve its location via the obsidian MCP tools — `mcp__obsidian__search_notes` (supports `searchFrontmatter: true`), `mcp__obsidian__read_note`, `mcp__obsidian__read_multiple_notes` — never assume an OS path.
3
+ An Obsidian vault holds session reports (`sessions/`), decisions (`decisions/`), and research (`Research/`) across projects, reached through the obsidian MCP tools.
4
4
 
5
- IMPORTANT: Before substantive project work, search the vault for prior sessions and decisions for the current project — by `project` frontmatter first, then keywords ("blocked", "superseded", "decision", "gotcha"). Also search when touching a component with known history or when a task might repeat or reverse a prior decision.
5
+ Before substantive project work, search the vault for prior sessions and decisions on the current project through the `recall` skill — by `project` frontmatter first, then by keyword. Search again when touching a component with known history, or when a task might repeat or reverse a prior decision.
6
6
 
7
- Session logging runs through `/session-log` (same full flow for short and long sessions); offer it at the end of substantive sessions. Reports include `vault_context_retrieved: true|false` and `session_id` (from `CLAUDE_CODE_SESSION_ID`; literal `unknown` when unset) in frontmatter, and every session log ends with a `/rename [Project] - [Primary Outcome]` command — mandatory output, never optional.
7
+ At the end of a substantive session, offer the `session-log` skill; it owns the report format, the `vault_context_retrieved` and `session_id` frontmatter, and the closing `/rename` hand-off.
@@ -0,0 +1,28 @@
1
+ # Verified-Commit-Gate Skip Marker
2
+
3
+ **When this applies:** A `git commit` or `git push` through the Bash tool is blocked by the `verified_commit_gate` hook.
4
+
5
+ ## The marker
6
+
7
+ Appending the comment `# verify-skip` as a trailing shell comment to the Bash command exempts that single commit or push from the gate. The hook (`hooks/blocking/verified_commit_gate.py`, marker constant `VERIFICATION_BYPASS_MARKER`) recognizes the marker only when its leading `#` sits at a word boundary and outside every quoted region — a genuine comment, not a data-only mention inside a quoted commit message or `gh` body — and lets the command run without a minted verdict.
8
+
9
+ ## When the marker is allowed
10
+
11
+ Use it only when both hold:
12
+
13
+ 1. The gate is blocking the command, and
14
+ 2. The branch surface content is the same code a `code-verifier` already passed clean — nothing effectively changed since that clean verdict.
15
+
16
+ That situation arises when the verdict fails to cover the current surface even though the code is the same: a clean verdict that never minted (the verifier's fenced block ran in a resumed turn, so the SubagentStop minter never fired), a surface hash perturbed by index-only staging churn, or a concurrent worktree write that was fully reverted back to the verified content. Before using the marker, confirm the match yourself: the test suite the verifier ran still passes, and the diff holds no content beyond what the clean verdict covered.
17
+
18
+ ## When the marker is not allowed
19
+
20
+ Every other case runs the verification: spawn the `code-verifier` agent and let the SubagentStop hook mint the verdict. In particular, never use the marker to:
21
+
22
+ - Skip a branch's first verification.
23
+ - Commit or push after any real code change since the last clean verdict — one changed line means a fresh verification.
24
+ - Work around a verifier that reported findings; findings get repaired and re-verified.
25
+
26
+ ## One command, one exemption
27
+
28
+ The marker exempts only the command that carries it. The next commit or push on the branch faces the gate again, so a follow-up change still verifies before it lands.
@@ -1,33 +1,5 @@
1
1
  # Workers Done Before Complete
2
2
 
3
- **When this applies:** Before marking any task `completed` when the task spawned workers — subagents, workflow agents, or background shells.
3
+ Before marking a task `completed` that spawned workers — subagents, workflow agents, or background shells — confirm both: every worker has finished, and each worker's result is merged into run state (`state.json`, `pr-converge-state.json`, the task list, or whatever record the task keeps). A worker still running, or one whose output never landed in run state, keeps the task `in_progress`: list the live workers, report any dead or hung one as a finding rather than dropping it in silence, and schedule a wakeup so the run picks the workers back up before the task closes.
4
4
 
5
- ## Rule
6
-
7
- A task reaches `completed` only when two things hold: every worker it spawned has finished, and each worker's result is merged into run state — `state.json`, `pr-converge-state.json`, the task list, or whatever record the task keeps. A worker that still runs, or one that finished but whose output never landed in run state, leaves the task open.
8
-
9
- List the live workers before you mark the task complete. When a worker is dead or hung, that is a finding to record and report, not a result to drop in silence. A step that waits on workers ends its turn `in_progress` with a wakeup scheduled, so the run picks the workers back up rather than closing the task without them.
10
-
11
- ## Checklist before marking complete
12
-
13
- | Check | Action |
14
- |---|---|
15
- | Are any spawned workers still running? | List them; if yes, stay `in_progress` and schedule a wakeup. |
16
- | Did every finished worker return a result? | Read each result; a dead or hung worker is a finding to report. |
17
- | Is each result merged into run state? | Write it to `state.json` / the task list before closing. |
18
- | Does the task's own goal now hold? | Confirm against the merged state, not a worker's self-report. |
19
-
20
- Mark `completed` only when every row passes.
21
-
22
- ## Examples
23
-
24
- **Wrong:** Marking the audit task complete while two bugteam workers still run in the background.
25
- **Right:** List the workers, see two still running, keep the task `in_progress`, and schedule a wakeup to collect them.
26
-
27
- **Wrong:** A worker crashes; the task closes as complete because the other workers finished.
28
- **Right:** Record the crashed worker as a finding, report it, and hold the task open until its work is covered.
29
-
30
- ## Relationship to other rules
31
-
32
- - **`long-horizon-autonomy`** covers acting on what you have and not ending a turn on a promise. This rule names the specific completion gate: workers finished and their results merged.
33
- - **`skills/pr-converge/reference/state-schema.md`** defines the run-state records this rule requires a worker's result to land in before the task closes.
5
+ Checklist, examples, and run-state detail: `@~/.claude/docs/worker-completion-gate.md`.
@@ -19,6 +19,12 @@ A usage-limited primary falls over to the second binary::
19
19
  primary claude -> exit 1, "usage limit reached" (falls over)
20
20
  fallback claude-ev -> exit 0 (served)
21
21
 
22
+ When stdin is piped (not a TTY), the runner reads it once and forwards the
23
+ same text to every chain attempt so a piped ``-p`` charter body reaches each
24
+ binary in the walk::
25
+
26
+ cat charter.md | python claude_chain_runner.py -- -p --strict-mcp-config
27
+
22
28
  Import ``run_claude`` for the outcome object, or run the module as a CLI::
23
29
 
24
30
  python claude_chain_runner.py [--timeout-seconds N] -- <claude args...>
@@ -27,11 +33,13 @@ Import ``run_claude`` for the outcome object, or run the module as a CLI::
27
33
  from __future__ import annotations
28
34
 
29
35
  import argparse
36
+ import io
30
37
  import json
31
38
  import subprocess
32
39
  import sys
33
40
  from dataclasses import dataclass
34
41
  from pathlib import Path
42
+ from typing import TextIO
35
43
 
36
44
  from dev_env_scripts_constants.claude_chain_constants import (
37
45
  ALL_USAGE_LIMIT_SIGNATURES,
@@ -48,6 +56,7 @@ from dev_env_scripts_constants.claude_chain_constants import (
48
56
  CLAUDE_HOME_SUBDIRECTORY,
49
57
  CLI_ARGUMENTS_SEPARATOR,
50
58
  CLI_TIMEOUT_FLAG,
59
+ CODEC_ERROR_STRATEGY,
51
60
  CONFIG_CHAIN_EMPTY_REASON,
52
61
  CONFIG_CHAIN_KEY,
53
62
  CONFIG_CHAIN_NOT_LIST_REASON,
@@ -283,7 +292,10 @@ def _classify_completion(
283
292
 
284
293
 
285
294
  def run_claude(
286
- all_claude_arguments: list[str], *, timeout_seconds: int
295
+ all_claude_arguments: list[str],
296
+ *,
297
+ timeout_seconds: int,
298
+ stdin_text: str | None = None,
287
299
  ) -> ChainInvocationOutcome:
288
300
  """Run *all_claude_arguments* through the configured fallback chain.
289
301
 
@@ -291,12 +303,16 @@ def run_claude(
291
303
  exit whose output carries a usage-limit signature) falls over to the next
292
304
  binary. A missing fallback binary is skipped and the walk continues. A
293
305
  timeout, a missing primary binary, or a non-zero exit without a usage-limit
294
- signature stops the walk and returns that outcome unchanged.
306
+ signature stops the walk and returns that outcome unchanged. When
307
+ *stdin_text* is set, that same text is supplied as stdin on every chain
308
+ attempt.
295
309
 
296
310
  Args:
297
311
  all_claude_arguments: Arguments passed after the binary name, such as
298
312
  ``["-p", prompt, "--strict-mcp-config"]``.
299
313
  timeout_seconds: Timeout applied to each binary invocation.
314
+ stdin_text: Optional UTF-8 text forwarded as stdin to every binary.
315
+ ``None`` leaves the subprocess without a piped stdin body.
300
316
 
301
317
  Returns:
302
318
  The outcome of the walk, naming the serving binary and the full
@@ -315,8 +331,11 @@ def run_claude(
315
331
  _build_invocation(each_entry, all_claude_arguments),
316
332
  capture_output=True,
317
333
  text=True,
334
+ encoding=UTF8_ENCODING,
335
+ errors=CODEC_ERROR_STRATEGY,
318
336
  timeout=timeout_seconds,
319
337
  check=False,
338
+ input=stdin_text,
320
339
  )
321
340
  except subprocess.TimeoutExpired as timeout_error:
322
341
  all_attempts.append(
@@ -368,6 +387,12 @@ def _exhausted_message(all_attempts: tuple[ChainAttempt, ...]) -> str:
368
387
  return CHAIN_EXHAUSTED_MESSAGE_TEMPLATE.format(attempt_summary=attempt_summary)
369
388
 
370
389
 
390
+ def _read_piped_stdin_text() -> str | None:
391
+ if sys.stdin.isatty():
392
+ return None
393
+ return sys.stdin.read()
394
+
395
+
371
396
  def main(all_command_arguments: list[str]) -> int:
372
397
  """Walk the chain for CLI arguments and return the process exit code.
373
398
 
@@ -381,9 +406,12 @@ def main(all_command_arguments: list[str]) -> int:
381
406
  parser = _build_argument_parser()
382
407
  parsed_arguments = parser.parse_args(all_command_arguments)
383
408
  all_claude_arguments = _strip_leading_separator(parsed_arguments.passthrough)
409
+ maybe_stdin_text = _read_piped_stdin_text()
384
410
  try:
385
411
  chain_outcome = run_claude(
386
- all_claude_arguments, timeout_seconds=parsed_arguments.timeout_seconds
412
+ all_claude_arguments,
413
+ timeout_seconds=parsed_arguments.timeout_seconds,
414
+ stdin_text=maybe_stdin_text,
387
415
  )
388
416
  except ChainConfigurationError as configuration_error:
389
417
  print(str(configuration_error), file=sys.stderr)
@@ -396,5 +424,13 @@ def main(all_command_arguments: list[str]) -> int:
396
424
  return chain_outcome.returncode
397
425
 
398
426
 
427
+ def _reconfigure_stream_to_utf8(stream: TextIO) -> None:
428
+ """Reconfigure *stream* to emit UTF-8, replacing any unmappable character."""
429
+ if isinstance(stream, io.TextIOWrapper):
430
+ stream.reconfigure(encoding=UTF8_ENCODING, errors=CODEC_ERROR_STRATEGY)
431
+
432
+
399
433
  if __name__ == "__main__":
434
+ _reconfigure_stream_to_utf8(sys.stdout)
435
+ _reconfigure_stream_to_utf8(sys.stderr)
400
436
  sys.exit(main(sys.argv[1:]))
@@ -9,6 +9,9 @@ from __future__ import annotations
9
9
  UTF8_ENCODING: str = "utf-8"
10
10
  """Encoding used to read the chain configuration file."""
11
11
 
12
+ CODEC_ERROR_STRATEGY: str = "replace"
13
+ """Codec error handler that maps any unencodable or undecodable value to a marker."""
14
+
12
15
  CLAUDE_HOME_SUBDIRECTORY: str = ".claude"
13
16
  """Per-user directory under the home directory that holds the chain config."""
14
17