scar-cli 0.22.0__tar.gz → 0.23.0__tar.gz

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 (159) hide show
  1. scar_cli-0.23.0/.release-please-manifest.json +3 -0
  2. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0017-codex-pretool-context-can-replay-apply-patch.landmine.md +0 -1
  3. scar_cli-0.22.0/.scars/candidates/one-directional-health-check-leaves-flattering-direction-unwatched.md → scar_cli-0.23.0/.scars/0020-one-directional-health-check-leaves-flattering-direction-unwatched.landmine.md +6 -2
  4. scar_cli-0.22.0/.scars/candidates/posttool-fix-in-hooks-does-not-reach-codex-or-cascade.md → scar_cli-0.23.0/.scars/0021-posttool-fix-in-hooks-does-not-reach-codex-or-cascade.landmine.md +5 -3
  5. scar_cli-0.22.0/.scars/candidates/codex-hook-delivery-needs-user-file-and-trust.md → scar_cli-0.23.0/.scars/0022-codex-hook-delivery-needs-user-file-and-trust.landmine.md +4 -3
  6. scar_cli-0.23.0/.scars/candidates/one-plugin-hook-manifest-two-hosts-two-root-variables.md +30 -0
  7. {scar_cli-0.22.0 → scar_cli-0.23.0}/CHANGELOG.md +11 -0
  8. {scar_cli-0.22.0 → scar_cli-0.23.0}/PKG-INFO +6 -12
  9. {scar_cli-0.22.0 → scar_cli-0.23.0}/README.md +5 -11
  10. {scar_cli-0.22.0 → scar_cli-0.23.0}/SPEC.md +4 -0
  11. {scar_cli-0.22.0 → scar_cli-0.23.0}/plugin/plugin.json +1 -1
  12. {scar_cli-0.22.0 → scar_cli-0.23.0}/pyproject.toml +1 -1
  13. {scar_cli-0.22.0 → scar_cli-0.23.0}/release-please-config.json +1 -0
  14. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/cli.py +188 -37
  15. scar_cli-0.23.0/src/scar/hosts.py +210 -0
  16. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/installer.py +31 -0
  17. scar_cli-0.23.0/tests/test_hosts.py +359 -0
  18. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_installer.py +285 -22
  19. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_plugin.py +11 -23
  20. {scar_cli-0.22.0 → scar_cli-0.23.0}/uv.lock +1 -1
  21. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/docs/agents.md +6 -6
  22. scar_cli-0.22.0/.release-please-manifest.json +0 -3
  23. scar_cli-0.22.0/plugin/hooks/hooks.json +0 -43
  24. {scar_cli-0.22.0 → scar_cli-0.23.0}/.claude-plugin/marketplace.json +0 -0
  25. {scar_cli-0.22.0 → scar_cli-0.23.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  26. {scar_cli-0.22.0 → scar_cli-0.23.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  27. {scar_cli-0.22.0 → scar_cli-0.23.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  28. {scar_cli-0.22.0 → scar_cli-0.23.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {scar_cli-0.22.0 → scar_cli-0.23.0}/.github/workflows/ci.yml +0 -0
  30. {scar_cli-0.22.0 → scar_cli-0.23.0}/.github/workflows/docs.yml +0 -0
  31. {scar_cli-0.22.0 → scar_cli-0.23.0}/.github/workflows/pr-validation.yml +0 -0
  32. {scar_cli-0.22.0 → scar_cli-0.23.0}/.github/workflows/release.yml +0 -0
  33. {scar_cli-0.22.0 → scar_cli-0.23.0}/.gitignore +0 -0
  34. {scar_cli-0.22.0 → scar_cli-0.23.0}/.pre-commit-config.yaml +0 -0
  35. {scar_cli-0.22.0 → scar_cli-0.23.0}/.pre-commit-hooks.yaml +0 -0
  36. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0001-git-grep-ere-pitfalls.landmine.md +0 -0
  37. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0002-agent-direct-hook-install.deadend.md +0 -0
  38. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0003-installer-binds-to-active-venv-scar.landmine.md +0 -0
  39. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0004-promote-roundtrip-drops-expires-evidence.landmine.md +0 -0
  40. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0005-history-rewrite-orphans-commit-evidence.landmine.md +0 -0
  41. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0006-yaml-pattern-anchor-over-escaping.landmine.md +0 -0
  42. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0007-release-please-config-change-skips-open-pr.landmine.md +0 -0
  43. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0008-rich-output-nontty-must-bypass-rich.landmine.md +0 -0
  44. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0009-anchor-survival-harness-measures-symbol-not-path-anchors.deadend.md +0 -0
  45. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0010-git-returncode-128-vs-empty-tracked-set.landmine.md +0 -0
  46. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0011-lint-compile-check-lets-redos-anchors-hang-hotpath.landmine.md +0 -0
  47. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0012-firing-log-readers-need-blanket-guards.landmine.md +0 -0
  48. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0013-docusaurus-future-v4-requires-faster-dep.landmine.md +0 -0
  49. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0014-inject-flag-dest-collides-with-subcommand-dispatch.landmine.md +0 -0
  50. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0015-zero-checkruns-means-approval-gate-not-workflow-bug.deadend.md +0 -0
  51. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0016-hook-ownership-must-be-kind-scoped.landmine.md +0 -0
  52. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0018-cli-subparsers-register-through-_add-wrapper.landmine.md +0 -0
  53. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/0019-token-in-scar-body-cannot-prove-hook-delivery.deadend.md +0 -0
  54. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/README.md +0 -0
  55. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/candidates/cascade-block-exit-collides-with-argparse.md +0 -0
  56. {scar_cli-0.22.0 → scar_cli-0.23.0}/.scars/template.md +0 -0
  57. {scar_cli-0.22.0 → scar_cli-0.23.0}/AGENTS.md +0 -0
  58. {scar_cli-0.22.0 → scar_cli-0.23.0}/CONTRIBUTING.md +0 -0
  59. {scar_cli-0.22.0 → scar_cli-0.23.0}/IDEA.md +0 -0
  60. {scar_cli-0.22.0 → scar_cli-0.23.0}/LICENSE +0 -0
  61. {scar_cli-0.22.0 → scar_cli-0.23.0}/ROADMAP.md +0 -0
  62. {scar_cli-0.22.0 → scar_cli-0.23.0}/SCAR-FORMAT.md +0 -0
  63. {scar_cli-0.22.0 → scar_cli-0.23.0}/STRESS-TEST.md +0 -0
  64. {scar_cli-0.22.0 → scar_cli-0.23.0}/action.yml +0 -0
  65. {scar_cli-0.22.0 → scar_cli-0.23.0}/codecov.yml +0 -0
  66. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/anchor-survival/PROTOCOL.md +0 -0
  67. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/anchor-survival/RESULTS.md +0 -0
  68. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/anchor-survival/long_replay.py +0 -0
  69. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/anchor-survival/replay.py +0 -0
  70. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/anchor-survival/replay_shipped.py +0 -0
  71. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/anchor-survival/test_replay_shipped.py +0 -0
  72. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/auto-authorship/FINDINGS.md +0 -0
  73. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/auto-authorship/PROTOCOL.md +0 -0
  74. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/.gitignore +0 -0
  75. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/PROTOCOL.md +0 -0
  76. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/RESULTS.md +0 -0
  77. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/fixture/.scars/0001-vendor-retry-window.fence.md +0 -0
  78. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/fixture/.scars/0002-evicting-session-store.deadend.md +0 -0
  79. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/fixture/.scars/0003-export-column-order.landmine.md +0 -0
  80. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/fixture/README.md +0 -0
  81. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/fixture/payments/retry.py +0 -0
  82. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/fixture/reports/export.py +0 -0
  83. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/fixture/services/sessions.py +0 -0
  84. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/fence-honor/grade.py +0 -0
  85. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/harvest/PROTOCOL.md +0 -0
  86. {scar_cli-0.22.0 → scar_cli-0.23.0}/experiments/harvest/harvest.py +0 -0
  87. {scar_cli-0.22.0 → scar_cli-0.23.0}/hook/scar-hooks.py +0 -0
  88. {scar_cli-0.22.0 → scar_cli-0.23.0}/plugin/hooks/run.sh +0 -0
  89. {scar_cli-0.22.0 → scar_cli-0.23.0}/plugin/skills/scar-authoring/SKILL.md +0 -0
  90. {scar_cli-0.22.0 → scar_cli-0.23.0}/plugin/skills/scar-authoring/assets/template.md +0 -0
  91. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/__init__.py +0 -0
  92. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/agent.py +0 -0
  93. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/cascade.py +0 -0
  94. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/codex.py +0 -0
  95. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/draftcheck.py +0 -0
  96. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/evidence.py +0 -0
  97. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/gc.py +0 -0
  98. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/harvest.py +0 -0
  99. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/hooks.py +0 -0
  100. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/lint.py +0 -0
  101. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/match.py +0 -0
  102. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/mcp.py +0 -0
  103. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/model.py +0 -0
  104. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/orphan.py +0 -0
  105. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/output.py +0 -0
  106. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/reanchor.py +0 -0
  107. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/renames.py +0 -0
  108. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/render.py +0 -0
  109. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/review.py +0 -0
  110. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/skills/scar-authoring/SKILL.md +0 -0
  111. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/skills/scar-authoring/assets/template.md +0 -0
  112. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/store.py +0 -0
  113. {scar_cli-0.22.0 → scar_cli-0.23.0}/src/scar/symbols.py +0 -0
  114. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/conftest.py +0 -0
  115. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_action_yml.py +0 -0
  116. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_anchor_kind.py +0 -0
  117. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_block_capability.py +0 -0
  118. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_cascade.py +0 -0
  119. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_cli.py +0 -0
  120. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_codex.py +0 -0
  121. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_context_size.py +0 -0
  122. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_docs.py +0 -0
  123. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_draftcheck.py +0 -0
  124. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_evidence.py +0 -0
  125. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_gc.py +0 -0
  126. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_git_hook.py +0 -0
  127. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_harvest.py +0 -0
  128. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_hooks.py +0 -0
  129. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_json_contract.py +0 -0
  130. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_lifecycle.py +0 -0
  131. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_lint.py +0 -0
  132. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_match.py +0 -0
  133. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_mcp.py +0 -0
  134. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_model.py +0 -0
  135. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_orphan.py +0 -0
  136. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_output.py +0 -0
  137. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_precommit_hooks.py +0 -0
  138. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_reanchor.py +0 -0
  139. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_renames.py +0 -0
  140. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_render.py +0 -0
  141. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_review.py +0 -0
  142. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_skill.py +0 -0
  143. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_store.py +0 -0
  144. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_symbols.py +0 -0
  145. {scar_cli-0.22.0 → scar_cli-0.23.0}/tests/test_verdict_observability.py +0 -0
  146. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/.gitignore +0 -0
  147. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/docs/concepts.md +0 -0
  148. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/docs/index.md +0 -0
  149. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/docs/methodology.md +0 -0
  150. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/docs/quickstart.md +0 -0
  151. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/docusaurus.config.ts +0 -0
  152. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/package-lock.json +0 -0
  153. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/package.json +0 -0
  154. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/scripts/sync-changelog.mjs +0 -0
  155. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/sidebars.ts +0 -0
  156. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/src/css/custom.css +0 -0
  157. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/static/img/favicon.svg +0 -0
  158. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/static/llms.txt +0 -0
  159. {scar_cli-0.22.0 → scar_cli-0.23.0}/website/tsconfig.json +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.23.0"
3
+ }
@@ -8,7 +8,6 @@ created: 2026-08-28
8
8
  authors: ["codex", "Kibukx"]
9
9
  anchors:
10
10
  - path: src/scar/codex.py
11
- - path: plugin/hooks/hooks.json
12
11
  evidence:
13
12
  - issue: 243
14
13
  - note: live codex-cli 0.147.0 A/B emitted two tool_use_ids for one identical apply_patch after the first PreToolUse context injection
@@ -1,21 +1,25 @@
1
1
  ---
2
- id: 0
2
+ id: 20
3
3
  type: landmine
4
4
  title: A health check that watches one direction of pipeline failure leaves the flattering direction unwatched
5
5
  severity: high
6
6
  confidence: 0.9
7
7
  created: 2026-08-30
8
8
  authors: ["claude-code"]
9
+ promoted_by: Kibukx
10
+ promoted_by_source: git-config
9
11
  anchors:
10
12
  - path: src/scar/cli.py
11
13
  - path: src/scar/hooks.py
14
+ - path: src/scar/codex.py
15
+ - path: src/scar/cascade.py
12
16
  - pattern: "instrument_disconnected|posttool_silent|verdict_observed"
13
17
  evidence:
14
18
  - issue: 277
15
19
  - issue: 237
16
20
  expires:
17
21
  condition: "the pipeline stops having two independently-installable halves"
18
- status: candidate
22
+ status: active
19
23
  ---
20
24
 
21
25
  The precheck and posttool hooks install separately and fail separately. #237
@@ -1,16 +1,17 @@
1
1
  ---
2
- id: 0
2
+ id: 21
3
3
  type: landmine
4
4
  title: A posttool change in hooks.py does not reach codex.py or cascade.py, which carry their own copy of the same logic
5
5
  severity: high
6
6
  confidence: 0.8
7
7
  created: 2026-09-02
8
8
  authors: ["claude-code"]
9
+ promoted_by: Kibukx
10
+ promoted_by_source: git-config
9
11
  anchors:
10
12
  - path: src/scar/hooks.py
11
13
  - path: src/scar/codex.py
12
14
  - path: src/scar/cascade.py
13
- violation: "if not violations:"
14
15
  evidence:
15
16
  - issue: 277
16
17
  - issue: 293
@@ -18,7 +19,8 @@ evidence:
18
19
  expires:
19
20
  condition: "the three posttool paths share one implementation, or the adapters delegate to hooks.posttool"
20
21
  review_after: 2026-12-01
21
- status: candidate
22
+ violation: "if not violations:"
23
+ status: active
22
24
  ---
23
25
 
24
26
  The posttool verdict logic exists three times: `hooks.posttool` for Claude
@@ -1,13 +1,14 @@
1
1
  ---
2
- id: 0
2
+ id: 22
3
3
  type: landmine
4
4
  title: Codex plugin hooks are not materialized, and untrusted Codex hooks are skipped silently
5
5
  severity: high
6
6
  confidence: 0.9
7
7
  created: 2026-08-28
8
8
  authors: ["claude-code"]
9
+ promoted_by: Kibukx
10
+ promoted_by_source: git-config
9
11
  anchors:
10
- - path: plugin/hooks/hooks.json
11
12
  - path: src/scar/codex.py
12
13
  - pattern: "hooks/hooks\.json|CODEX_HOME|codex-(pretool|posttool|session-notice)"
13
14
  evidence:
@@ -17,7 +18,7 @@ evidence:
17
18
  expires:
18
19
  condition: "Codex materializes a plugin's hooks/hooks.json into the installed cache AND surfaces an error for untrusted hooks"
19
20
  review_after: 2027-02-28
20
- status: candidate
21
+ status: active
21
22
  ---
22
23
 
23
24
  Shipping a Codex hook through a plugin's `hooks/hooks.json` does not deliver it,
@@ -0,0 +1,30 @@
1
+ ---
2
+ id: 0
3
+ type: landmine
4
+ title: A plugin hook manifest is read by every host that installs the plugin, and hosts expand different root variables
5
+ severity: high
6
+ confidence: 0.9
7
+ created: 2026-09-03
8
+ authors: ["claude-code"]
9
+ anchors:
10
+ - path: plugin/
11
+ - path: src/scar/installer.py
12
+ - pattern: "PLUGIN_ROOT"
13
+ evidence:
14
+ - issue: 303
15
+ - issue: 244
16
+ expires:
17
+ condition: "the plugin format gains per-host scoping for hook files"
18
+ review_after: 2026-12-01
19
+ status: candidate
20
+ ---
21
+
22
+ Claude Code merges plugin.json hooks with hooks/hooks.json and defines only
23
+ CLAUDE_PLUGIN_ROOT. Codex reads the same file and defines both PLUGIN_ROOT
24
+ and CLAUDE_PLUGIN_ROOT. A hooks/hooks.json written for Codex with
25
+ ${PLUGIN_ROOT} therefore ran on every Claude session start and Bash call as
26
+ "/hooks/run.sh": No such file or directory, on every machine with the plugin
27
+ (#244 introduced it, #303 removed it). There is no way to mark a plugin
28
+ hook file as one host's only. Ship one manifest per plugin, on
29
+ ${CLAUDE_PLUGIN_ROOT}, and deliver host-specific hooks through
30
+ `scar hook install --runtime <host>`, whose files live outside the plugin.
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.23.0](https://github.com/Daily-Nerd/Scar/compare/v0.22.0...v0.23.0) (2026-09-03)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **install:** with no --runtime, no terminal, and more than one detected host, scar hook install and scar skill install write nothing and print the commands to run instead of installing Claude Code by default.
9
+
10
+ ### Features
11
+
12
+ * **install:** detect installed hosts and the channel that already serves each ([#304](https://github.com/Daily-Nerd/Scar/issues/304)) ([180cf6f](https://github.com/Daily-Nerd/Scar/commit/180cf6f53ab671e6fb9ce6636fe718aabead1c7c))
13
+
3
14
  ## [0.22.0](https://github.com/Daily-Nerd/Scar/compare/v0.21.0...v0.22.0) (2026-09-03)
4
15
 
5
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scar-cli
3
- Version: 0.22.0
3
+ Version: 0.23.0
4
4
  Summary: SCAR — version control for negative knowledge (deadends, fences, landmines)
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -75,20 +75,14 @@ cp .scars/template.md .scars/candidates/redis-sessions.md
75
75
  $EDITOR .scars/candidates/redis-sessions.md
76
76
  scar lint # validate format
77
77
  scar promote redis-sessions.md # human review gate: candidate -> active
78
- scar hook install # Claude Code: inject scars before agent edits
79
- scar skill install # Claude Code: authoring skill into ~/.claude/skills/
80
- scar hook install --runtime windsurf # Windsurf/Cascade: block-once injection
78
+ scar hook install # detects installed hosts, asks which to wire (--all, --runtime X)
79
+ scar skill install # same detection for the scar-authoring skill
80
+ scar hook status # every host with the channel that serves it (settings, plugin, none)
81
81
  ```
82
82
 
83
- Claude Code users: the marketplace **plugin** ships the hooks and the
84
- scar-authoring skill together; the two commands above are the manual fallback.
85
- Re-run `scar skill install` after upgrading — the installed skill is a static copy.
83
+ Claude Code users: the marketplace **plugin** ships the hooks and the scar-authoring skill together. `scar hook install` and `scar skill install` see the plugin and skip Claude unless you pass `--runtime claude --force`. Re-run `scar skill install` after upgrading a manual install; the installed skill is a static copy.
86
84
 
87
- Codex users: install and enable the Scar plugin, then open `/hooks` to review
88
- and trust its current hook definitions. The native plugin hooks push matching
89
- scars before `Bash` and `apply_patch` calls and report post-patch `violation:`
90
- tripwires without blocking or rewriting the action. Plugin updates that change
91
- the hook definitions require trust review again.
85
+ Codex users: run `scar hook install --runtime codex`, then open `/hooks` in Codex to review and trust the entries. The hooks push matching scars before `Bash` and `apply_patch` calls and report post-patch `violation:` tripwires without blocking. The plugin no longer carries Codex hook definitions; the installer is the one path.
92
86
 
93
87
  Full walkthrough, lifecycle commands, and agent wiring (Claude Code plugin,
94
88
  Codex native hooks, Windsurf/Cascade hooks, MCP server, `scar draft-check`):
@@ -60,20 +60,14 @@ cp .scars/template.md .scars/candidates/redis-sessions.md
60
60
  $EDITOR .scars/candidates/redis-sessions.md
61
61
  scar lint # validate format
62
62
  scar promote redis-sessions.md # human review gate: candidate -> active
63
- scar hook install # Claude Code: inject scars before agent edits
64
- scar skill install # Claude Code: authoring skill into ~/.claude/skills/
65
- scar hook install --runtime windsurf # Windsurf/Cascade: block-once injection
63
+ scar hook install # detects installed hosts, asks which to wire (--all, --runtime X)
64
+ scar skill install # same detection for the scar-authoring skill
65
+ scar hook status # every host with the channel that serves it (settings, plugin, none)
66
66
  ```
67
67
 
68
- Claude Code users: the marketplace **plugin** ships the hooks and the
69
- scar-authoring skill together; the two commands above are the manual fallback.
70
- Re-run `scar skill install` after upgrading — the installed skill is a static copy.
68
+ Claude Code users: the marketplace **plugin** ships the hooks and the scar-authoring skill together. `scar hook install` and `scar skill install` see the plugin and skip Claude unless you pass `--runtime claude --force`. Re-run `scar skill install` after upgrading a manual install; the installed skill is a static copy.
71
69
 
72
- Codex users: install and enable the Scar plugin, then open `/hooks` to review
73
- and trust its current hook definitions. The native plugin hooks push matching
74
- scars before `Bash` and `apply_patch` calls and report post-patch `violation:`
75
- tripwires without blocking or rewriting the action. Plugin updates that change
76
- the hook definitions require trust review again.
70
+ Codex users: run `scar hook install --runtime codex`, then open `/hooks` in Codex to review and trust the entries. The hooks push matching scars before `Bash` and `apply_patch` calls and report post-patch `violation:` tripwires without blocking. The plugin no longer carries Codex hook definitions; the installer is the one path.
77
71
 
78
72
  Full walkthrough, lifecycle commands, and agent wiring (Claude Code plugin,
79
73
  Codex native hooks, Windsurf/Cascade hooks, MCP server, `scar draft-check`):
@@ -464,3 +464,7 @@ just as well as edit rows do.
464
464
  command output, and asserts the nullable semantics of §9.2. A guarantee no
465
465
  test enforces is an untestable assertion, and untestable assertions are the
466
466
  ones that reach `main` wrong.
467
+
468
+ ### 9.10 Install detection
469
+
470
+ `scar hook install` and `scar skill install` without `--runtime` detect hosts (claude, codex, windsurf, cursor, opencode) by config directory or binary on PATH, and resolve for each the channel that already serves it: `settings` (Scar's own entries in the host's hook file), `plugin` (the scar marketplace plugin installed and not disabled), or `none`. windsurf is repo-scoped: its hooks file is written into the working repo, so the binary on PATH counts only when that repo is inside a git repository. On a terminal, one question per present, wirable, unserved host, default no. Without a terminal, exactly one such host is installed; zero or several means the commands are printed and nothing is written. Zero detected hosts says so, rather than naming the plugin. `--all` wires every such host. `--runtime claude` on a Claude Code already served by the plugin refuses unless `--force`. `--all` and `--force` apply to `install` only, and `--force` applies to `--runtime claude` only. A missing or unreadable plugin registry never counts as `plugin`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scar",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "SCAR — version control for negative knowledge (deadends, fences, landmines)",
5
5
  "hooks": {
6
6
  "PreToolUse": [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scar-cli"
3
- version = "0.22.0"
3
+ version = "0.23.0"
4
4
  description = "SCAR — version control for negative knowledge (deadends, fences, landmines)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -3,6 +3,7 @@
3
3
  "packages": {
4
4
  ".": {
5
5
  "release-type": "python",
6
+ "bump-minor-pre-major": true,
6
7
  "extra-files": [
7
8
  {
8
9
  "type": "json",
@@ -9,11 +9,13 @@ from __future__ import annotations
9
9
 
10
10
  import argparse
11
11
  import json
12
+ import os
12
13
  import sys
13
14
  import time
14
15
  from datetime import datetime
15
16
  from importlib.metadata import PackageNotFoundError, version as _dist_version
16
17
  from pathlib import Path
18
+ from typing import Callable
17
19
 
18
20
  from rich_argparse import RichHelpFormatter
19
21
 
@@ -2666,46 +2668,166 @@ def _cmd_cascade_hook(args) -> int:
2666
2668
  return cascade_hook()
2667
2669
 
2668
2670
 
2671
+ def _detect(kind: str):
2672
+ """Which hosts exist here, and which channel already serves each one.
2673
+ Pure read: hosts.py never writes and never prompts (#303)."""
2674
+ from . import hosts, installer
2675
+ home = installer.CLAUDE_DIR.parent
2676
+ repo = Path.cwd()
2677
+ found = hosts.detect_hosts(home, repo, kind=kind,
2678
+ path_env=os.environ.get("PATH", ""),
2679
+ codex_dir=installer.codex_home())
2680
+ resolved = hosts.resolve_channels(found, claude_dir=installer.CLAUDE_DIR,
2681
+ repo=repo, kind=kind)
2682
+ return resolved, repo
2683
+
2684
+
2685
+ def _run_hook_installers(names: list[str], repo: Path, dry: bool) -> int:
2686
+ """Worst exit code wins: one host failing must not be reported as success."""
2687
+ from .installer import cascade_install, codex_install, install
2688
+ rc = 0
2689
+ for name in names:
2690
+ print(f"== {name}")
2691
+ if name == "claude":
2692
+ rc = max(rc, install(dry=dry))
2693
+ elif name == "codex":
2694
+ rc = max(rc, codex_install(dry=dry))
2695
+ elif name == "windsurf":
2696
+ rc = max(rc, cascade_install(repo, dry=dry))
2697
+ return rc
2698
+
2699
+
2700
+ def _lifecycle_flag_guard(args) -> int | None:
2701
+ """--all/--runtime exclusivity is argparse's job. The rest fails here, and
2702
+ by returning: every command in this CLI returns exit codes and never
2703
+ raises to the shell. Shared verbatim by `hook` and `skill` (#303).
2704
+
2705
+ Both --all and --force describe how to install, so on uninstall or status
2706
+ they can only be silently ignored. --force overrides the plugin check,
2707
+ and the plugin channel is Claude Code's alone, so on any other runtime it
2708
+ would claim to override something that does not exist."""
2709
+ force = getattr(args, "force", False)
2710
+ for flag, given in (("--all", getattr(args, "all", False)), ("--force", force)):
2711
+ if given and args.kind != "install":
2712
+ print(f"{flag} applies to install only")
2713
+ return 2
2714
+ if force and not args.runtime:
2715
+ print("--force needs --runtime: it overrides the plugin check for one "
2716
+ "named runtime, and detection never needs overriding.")
2717
+ return 2
2718
+ if force and args.runtime != "claude":
2719
+ print("--force applies to --runtime claude only, the plugin channel "
2720
+ "is Claude Code only")
2721
+ return 2
2722
+ return None
2723
+
2724
+
2725
+ def _lifecycle_no_runtime(kind: str, args,
2726
+ install_many: Callable[[list[str], Path, bool], int],
2727
+ status_fn: Callable[[], int]) -> int:
2728
+ """No --runtime on install/status: look at the machine first, print what
2729
+ is there, then either decide-and-install or fall through to the existing
2730
+ per-runtime status. Plain print, not Rich, because the non-tty branch of
2731
+ a read command must stay unwrapped (scar 0008). `kind` doubles as the
2732
+ `hosts.decide` command name, matching the subcommand it is called under
2733
+ (`hook`/`skill`)."""
2734
+ from . import hosts
2735
+ found, repo = _detect(kind)
2736
+ print(hosts.render_table(found))
2737
+ if args.kind == "install":
2738
+ decision = hosts.decide(found, interactive=hosts.is_interactive(),
2739
+ all_flag=getattr(args, "all", False),
2740
+ ask=hosts.ask_yes_no, command=kind)
2741
+ for line in decision.lines:
2742
+ print(line)
2743
+ return install_many(decision.install, repo, args.dry_run)
2744
+ print()
2745
+ return status_fn()
2746
+
2747
+
2748
+ def _plugin_refusal(subject: str, target: str) -> None:
2749
+ """The one-line refusal printed when `--runtime claude install` would
2750
+ write over a channel the scar plugin already serves. `subject` reads
2751
+ into "claude: {subject} provided by the scar plugin...", so it carries
2752
+ its own verb agreement ("hooks are", "the scar-authoring skill is")."""
2753
+ print(f"claude: {subject} provided by the scar plugin (scar@scar); "
2754
+ f"nothing written. Pass --force to install into {target} as well.")
2755
+
2756
+
2669
2757
  def _cmd_hook_lifecycle(args) -> int:
2670
- runtime = getattr(args, "runtime", "claude")
2758
+ from . import hosts
2759
+ from .installer import (
2760
+ CLAUDE_DIR,
2761
+ cascade_install,
2762
+ cascade_status,
2763
+ cascade_uninstall,
2764
+ codex_install,
2765
+ codex_status,
2766
+ codex_uninstall,
2767
+ git_hook_install,
2768
+ git_hook_status,
2769
+ git_hook_uninstall,
2770
+ install,
2771
+ status,
2772
+ uninstall,
2773
+ )
2774
+ dry = args.dry_run
2775
+ if (rc := _lifecycle_flag_guard(args)) is not None:
2776
+ return rc
2777
+ if getattr(args, "git", False):
2778
+ repo = Path.cwd()
2779
+ return {"install": lambda: git_hook_install(repo, dry=dry),
2780
+ "uninstall": lambda: git_hook_uninstall(repo, dry=dry),
2781
+ "status": lambda: git_hook_status(repo)}[args.kind]()
2782
+ runtime = args.runtime
2783
+ if runtime is None and args.kind in ("install", "status"):
2784
+ return _lifecycle_no_runtime("hook", args, _run_hook_installers, status)
2785
+ # uninstall with no --runtime keeps targeting Claude: removal is out of
2786
+ # the detection spec's scope, and widening it silently would rip hooks
2787
+ # out of hosts the user never named.
2788
+ runtime = runtime or "claude"
2789
+ if (runtime == "claude" and args.kind == "install"
2790
+ and not getattr(args, "force", False)
2791
+ and hosts.claude_plugin_enabled(CLAUDE_DIR)):
2792
+ _plugin_refusal("hooks are", "~/.claude/settings.json")
2793
+ return 0
2671
2794
  if runtime == "codex":
2672
- from .installer import codex_install, codex_status, codex_uninstall
2673
- if args.kind == "install":
2674
- return codex_install(dry=args.dry_run)
2675
- if args.kind == "uninstall":
2676
- return codex_uninstall(dry=args.dry_run)
2677
- return codex_status()
2795
+ return {"install": lambda: codex_install(dry=dry),
2796
+ "uninstall": lambda: codex_uninstall(dry=dry),
2797
+ "status": codex_status}[args.kind]()
2678
2798
  if runtime == "windsurf":
2679
- from .installer import cascade_install, cascade_status, cascade_uninstall
2680
- repo = Path.cwd()
2681
- if args.kind == "install":
2682
- return cascade_install(repo, dry=args.dry_run)
2683
- if args.kind == "uninstall":
2684
- return cascade_uninstall(repo, dry=args.dry_run)
2685
- return cascade_status(repo)
2686
- if getattr(args, "git", False):
2687
- from .installer import git_hook_install, git_hook_status, git_hook_uninstall
2688
2799
  repo = Path.cwd()
2689
- if args.kind == "install":
2690
- return git_hook_install(repo, dry=args.dry_run)
2691
- if args.kind == "uninstall":
2692
- return git_hook_uninstall(repo, dry=args.dry_run)
2693
- return git_hook_status(repo)
2694
- from .installer import install, status, uninstall
2695
- if args.kind == "install":
2696
- return install(dry=args.dry_run)
2697
- if args.kind == "uninstall":
2698
- return uninstall(dry=args.dry_run)
2699
- return status()
2800
+ return {"install": lambda: cascade_install(repo, dry=dry),
2801
+ "uninstall": lambda: cascade_uninstall(repo, dry=dry),
2802
+ "status": lambda: cascade_status(repo)}[args.kind]()
2803
+ return {"install": lambda: install(dry=dry),
2804
+ "uninstall": lambda: uninstall(dry=dry),
2805
+ "status": status}[args.kind]()
2700
2806
 
2701
2807
 
2702
2808
  def _cmd_skill_lifecycle(args) -> int:
2703
- from .installer import skill_install, skill_status, skill_uninstall
2704
- if args.kind == "install":
2705
- return skill_install(dry=args.dry_run)
2706
- if args.kind == "uninstall":
2707
- return skill_uninstall(dry=args.dry_run)
2708
- return skill_status()
2809
+ from . import hosts
2810
+ from .installer import CLAUDE_DIR, skill_install, skill_status, skill_uninstall
2811
+ dry = args.dry_run
2812
+ if (rc := _lifecycle_flag_guard(args)) is not None:
2813
+ return rc
2814
+ runtime = args.runtime
2815
+ if runtime is None and args.kind in ("install", "status"):
2816
+ def install_many(names: list[str], repo: Path, dry: bool) -> int:
2817
+ del repo # skill has exactly one wirable host: claude
2818
+ return skill_install(dry=dry) if "claude" in names else 0
2819
+ return _lifecycle_no_runtime("skill", args, install_many, skill_status)
2820
+ # uninstall with no --runtime keeps targeting Claude, the only wirable
2821
+ # host today: same rule as `hook`.
2822
+ runtime = runtime or "claude"
2823
+ if (runtime == "claude" and args.kind == "install"
2824
+ and not getattr(args, "force", False)
2825
+ and hosts.claude_plugin_enabled(CLAUDE_DIR)):
2826
+ _plugin_refusal("the scar-authoring skill is", "~/.claude/skills")
2827
+ return 0
2828
+ return {"install": lambda: skill_install(dry=dry),
2829
+ "uninstall": lambda: skill_uninstall(dry=dry),
2830
+ "status": skill_status}[args.kind]()
2709
2831
 
2710
2832
 
2711
2833
  def _scar_version() -> str:
@@ -2866,22 +2988,34 @@ def build_parser() -> argparse.ArgumentParser:
2866
2988
  p.add_argument("--dry-run", action="store_true",
2867
2989
  help="show lifecycle changes without writing settings")
2868
2990
  # One invocation, one target: --git writes .git/hooks/post-commit,
2869
- # --runtime windsurf writes .windsurf/hooks.json, and the default writes
2870
- # ~/.claude/settings.json. Passing two can only mean one is ignored.
2991
+ # --runtime windsurf writes .windsurf/hooks.json, --all writes to every
2992
+ # unserved host detection finds, and no flag at all means detect and ask.
2993
+ # Passing two can only mean one is ignored.
2871
2994
  target = p.add_mutually_exclusive_group()
2872
2995
  target.add_argument("--git", action="store_true",
2873
2996
  help="with install/uninstall/status: target this repo's "
2874
2997
  ".git/hooks/post-commit trigger for `scar draft-check` "
2875
2998
  "(#117) instead of Claude Code's settings.json")
2876
2999
  target.add_argument("--runtime", choices=["claude", "codex", "windsurf"],
2877
- default="claude",
3000
+ default=None,
2878
3001
  help="with install/uninstall/status: which runtime to wire. "
3002
+ "Omitted: detect installed hosts and ask (install), or "
3003
+ "show every host's channel (status); uninstall still "
3004
+ "targets claude. "
2879
3005
  "codex writes ~/.codex/hooks.json — shared with other "
2880
3006
  "tools, so merged, and inactive until you trust the "
2881
3007
  "entries in Codex's `/hooks` (#246); "
2882
3008
  "windsurf writes this repo's committed "
2883
3009
  ".windsurf/hooks.json (Cascade block-once, #197); "
2884
- "claude (default) writes ~/.claude/settings.json")
3010
+ "claude writes ~/.claude/settings.json")
3011
+ target.add_argument("--all", action="store_true",
3012
+ help="with install: wire every detected, unserved host "
3013
+ "without asking. A target like the other two, so "
3014
+ "argparse rejects it next to --runtime or --git "
3015
+ "rather than letting one be silently ignored")
3016
+ p.add_argument("--force", action="store_true",
3017
+ help="with --runtime claude: install into the settings file even "
3018
+ "though the scar plugin already serves that host")
2885
3019
 
2886
3020
  _add(sub, "cascade-hook", _cmd_cascade_hook,
2887
3021
  help="run the Windsurf/Cascade hook adapter (stdin JSON; installed by "
@@ -2891,6 +3025,23 @@ def build_parser() -> argparse.ArgumentParser:
2891
3025
  p.add_argument("kind", choices=["install", "uninstall", "status"])
2892
3026
  p.add_argument("--dry-run", action="store_true",
2893
3027
  help="show changes without writing to ~/.claude/skills")
3028
+ # One invocation, one target: --runtime claude writes ~/.claude/skills,
3029
+ # --all writes to every unserved host detection finds (only claude is
3030
+ # wirable today), and no flag at all means detect and ask.
3031
+ target = p.add_mutually_exclusive_group()
3032
+ target.add_argument("--runtime", choices=["claude"], default=None,
3033
+ help="with install/uninstall/status: which host to wire. "
3034
+ "Omitted: detect installed hosts and ask (install), "
3035
+ "or show every host's channel (status); uninstall "
3036
+ "still targets claude")
3037
+ target.add_argument("--all", action="store_true",
3038
+ help="with install: wire every detected, unserved host "
3039
+ "without asking. A target like --runtime, so "
3040
+ "argparse rejects it next to --runtime rather than "
3041
+ "letting one be silently ignored")
3042
+ p.add_argument("--force", action="store_true",
3043
+ help="with --runtime claude: install even though the scar plugin "
3044
+ "already ships the skill")
2894
3045
 
2895
3046
  _add(sub, "mcp", _cmd_mcp, help="run the SCAR MCP stdio server")
2896
3047