capt-hook 3.5.0__tar.gz → 3.7.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 (91) hide show
  1. {capt_hook-3.5.0 → capt_hook-3.7.0}/PKG-INFO +1 -1
  2. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/cli.py +16 -12
  3. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/manager.py +25 -9
  4. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/cli.py +6 -6
  5. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/SKILL.md +2 -2
  6. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +1 -1
  7. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +1 -1
  8. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +7 -7
  9. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +1 -1
  10. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/translating-styleguides/SKILL.md +1 -1
  11. {capt_hook-3.5.0 → capt_hook-3.7.0}/pyproject.toml +1 -1
  12. {capt_hook-3.5.0 → capt_hook-3.7.0}/LICENSE +0 -0
  13. {capt_hook-3.5.0 → capt_hook-3.7.0}/README.md +0 -0
  14. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/.claude-plugin/plugin.json +0 -0
  15. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/__init__.py +0 -0
  16. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/__main__.py +0 -0
  17. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/app.py +0 -0
  18. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/classifiers/__init__.py +0 -0
  19. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/classifiers/conductor.py +0 -0
  20. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/classifiers/droid.py +0 -0
  21. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/classifiers/native.py +0 -0
  22. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/command.py +0 -0
  23. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/conditions.py +0 -0
  24. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/context.py +0 -0
  25. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/decisions.py +0 -0
  26. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/dispatch.py +0 -0
  27. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/events.py +0 -0
  28. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/file.py +0 -0
  29. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/llm/__init__.py +0 -0
  30. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/loader.py +0 -0
  31. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/log.py +0 -0
  32. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/__init__.py +0 -0
  33. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/capt-hook.toml +0 -0
  34. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/commands.py +0 -0
  35. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/docs.py +0 -0
  36. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/plans.py +0 -0
  37. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/prompts.py +0 -0
  38. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/review.py +0 -0
  39. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/stewardship.py +0 -0
  40. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/general/tasks.py +0 -0
  41. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/python/capt-hook.toml +0 -0
  42. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/python/style.py +0 -0
  43. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/python/testing.py +0 -0
  44. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/packs/python/toolchain.py +0 -0
  45. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/__init__.py +0 -0
  46. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/commands.py +0 -0
  47. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/lint.py +0 -0
  48. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/llm.py +0 -0
  49. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/nudge.py +0 -0
  50. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/primitives/workflow.py +0 -0
  51. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/prompt.py +0 -0
  52. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/py.typed +0 -0
  53. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/__init__.py +0 -0
  54. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/dashboard.py +0 -0
  55. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/fix.py +0 -0
  56. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/formats.py +0 -0
  57. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/judge.py +0 -0
  58. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/pipeline.py +0 -0
  59. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/repo.py +0 -0
  60. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/scan.py +0 -0
  61. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/settings.py +0 -0
  62. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/store.py +0 -0
  63. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/review/sync.py +0 -0
  64. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/session.py +0 -0
  65. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/settings.py +0 -0
  66. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/signals/__init__.py +0 -0
  67. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/signals/nlp.py +0 -0
  68. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  69. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  70. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
  71. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
  72. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  73. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  74. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  75. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/state.py +0 -0
  76. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/style/__init__.py +0 -0
  77. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/style/matchers.py +0 -0
  78. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/style/scope.py +0 -0
  79. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/style/types.py +0 -0
  80. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/tasks.py +0 -0
  81. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
  82. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/testing/__init__.py +0 -0
  83. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/testing/helpers.py +0 -0
  84. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/testing/session_cache.py +0 -0
  85. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/testing/types.py +0 -0
  86. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/tests/__init__.py +0 -0
  87. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/tests/helpers.py +0 -0
  88. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/types.py +0 -0
  89. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/util/__init__.py +0 -0
  90. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/util/model_cache.py +0 -0
  91. {capt_hook-3.5.0 → capt_hook-3.7.0}/captain_hook/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capt-hook
3
- Version: 3.5.0
3
+ Version: 3.7.0
4
4
  Summary: Declarative hook framework for Claude Code
5
5
  Keywords: claude,claude-code,hooks,llm,agents,guardrails,cli
6
6
  Author: Yasyf Mohamedali
@@ -155,21 +155,25 @@ def capt_hook_events(path: Path) -> set[str]:
155
155
  }
156
156
 
157
157
 
158
+ def sibling_settings(path: Path) -> Path:
159
+ return path.parent / ("settings.json" if path.name == "settings.local.json" else "settings.local.json")
160
+
161
+
158
162
  def merge_settings(
159
163
  hooks_dir: str, settings_path: Path, from_source: str = DIST_NAME
160
164
  ) -> tuple[dict[str, Any], dict[str, str]]:
161
165
  new_hooks: dict[str, list[dict[str, Any]]] = generate_settings(hooks_dir, from_source=from_source)["hooks"]
162
166
  existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
163
167
  existing_hooks: dict[str, list[dict[str, Any]]] = existing.get("hooks") or {}
164
- committed = capt_hook_events(settings_path.parent / "settings.json")
168
+ deferred = capt_hook_events(sibling_settings(settings_path))
165
169
 
166
170
  summary: dict[str, str] = {}
167
171
  merged_hooks: dict[str, list[dict[str, Any]]] = {}
168
172
  for event in sorted(existing_hooks.keys() | new_hooks.keys()):
169
173
  foreign = [g for g in existing_hooks.get(event, []) if not is_captain_hook_group(g)]
170
174
  old_own = [g for g in existing_hooks.get(event, []) if is_captain_hook_group(g)]
171
- fresh_own = [] if event in committed else new_hooks.get(event, [])
172
- if event in committed and (old_own or new_hooks.get(event)):
175
+ fresh_own = [] if event in deferred else new_hooks.get(event, [])
176
+ if event in deferred and (old_own or new_hooks.get(event)):
173
177
  summary[event] = "deferred"
174
178
  elif old_own or fresh_own:
175
179
  summary[event] = (
@@ -193,7 +197,7 @@ def write_settings(settings_path: Path, data: dict[str, Any]) -> None:
193
197
  os.replace(tmp, settings_path)
194
198
 
195
199
 
196
- def print_hook_summary(label: str, summary: dict[str, str]) -> None:
200
+ def print_hook_summary(label: str, summary: dict[str, str], deferred_to: str) -> None:
197
201
  by_status: defaultdict[str, list[str]] = defaultdict(list)
198
202
  for event, status in summary.items():
199
203
  by_status[status].append(event)
@@ -209,15 +213,15 @@ def print_hook_summary(label: str, summary: dict[str, str]) -> None:
209
213
  if unchanged := by_status["unchanged"]:
210
214
  click.echo(f" unchanged: {', '.join(unchanged)} (already present)")
211
215
  if deferred := by_status["deferred"]:
212
- click.echo(f" deferred to settings.json: {', '.join(deferred)}")
216
+ click.echo(f" deferred to {deferred_to}: {', '.join(deferred)}")
213
217
 
214
218
 
215
219
  def regenerate_settings(state: CliState) -> None:
216
220
  state.discover()
217
- settings_path = state.root / ".claude" / "settings.local.json"
221
+ settings_path = state.root / ".claude" / "settings.json"
218
222
  merged, summary = merge_settings(".claude/hooks", settings_path)
219
223
  write_settings(settings_path, merged)
220
- print_hook_summary(str(settings_path.relative_to(state.root)), summary)
224
+ print_hook_summary(str(settings_path.relative_to(state.root)), summary, sibling_settings(settings_path).name)
221
225
 
222
226
 
223
227
  def settings_drift(root: Path) -> set[str]:
@@ -309,7 +313,7 @@ def init_project(root: Path, *, review: bool = True) -> None:
309
313
  if not example.exists():
310
314
  example.write_text(example_hook_source())
311
315
 
312
- settings_path = root / ".claude" / "settings.local.json"
316
+ settings_path = root / ".claude" / "settings.json"
313
317
  CliState(root=root, hooks=str(hooks_dir)).discover()
314
318
  merged, summary = merge_settings(".claude/hooks", settings_path)
315
319
  write_settings(settings_path, merged)
@@ -318,7 +322,7 @@ def init_project(root: Path, *, review: bool = True) -> None:
318
322
 
319
323
  click.echo(f"Scaffolded {example.relative_to(root)} + {settings_path.relative_to(root)}.")
320
324
  click.echo()
321
- print_hook_summary(str(settings_path.relative_to(root)), summary)
325
+ print_hook_summary(str(settings_path.relative_to(root)), summary, sibling_settings(settings_path).name)
322
326
  click.echo()
323
327
  click.echo("Claude Code plugin:")
324
328
  click.echo(f" + registered {PLUGIN_ID} in .claude/settings.json (skills install on folder-trust)")
@@ -476,15 +480,15 @@ def run(state: CliState, event: str, async_: bool) -> None:
476
480
  )
477
481
  @click.pass_obj
478
482
  def register_hooks_cmd(state: CliState, hooks_dir: str, dry_run: bool, from_source: str) -> None:
479
- """Register captain-hook's event hooks into .claude/settings.local.json."""
483
+ """Register captain-hook's event hooks into .claude/settings.json."""
480
484
  state.discover()
481
- settings_path = state.root / ".claude" / "settings.local.json"
485
+ settings_path = state.root / ".claude" / "settings.json"
482
486
  merged, summary = merge_settings(hooks_dir, settings_path, from_source=from_source)
483
487
  if dry_run:
484
488
  click.echo(json.dumps(merged, indent=2))
485
489
  return
486
490
  write_settings(settings_path, merged)
487
- print_hook_summary(str(settings_path), summary)
491
+ print_hook_summary(str(settings_path), summary, sibling_settings(settings_path).name)
488
492
 
489
493
 
490
494
  @cli.command()
@@ -27,6 +27,8 @@ from captain_hook import state
27
27
 
28
28
  PACKS_TOML = "packs.toml"
29
29
  PACK_MANIFEST = "capt-hook.toml"
30
+ # A pack's manifest may sit in .claude/ (preferred) or at the repo root.
31
+ MANIFEST_MEMBERS = (f".claude/{PACK_MANIFEST}", PACK_MANIFEST)
30
32
  SHA_MARKER = ".sha"
31
33
  SOURCE_RE = re.compile(r"^github:(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+?)(?:@(?P<ref>[\w./-]+))?$")
32
34
  PACK_NAME_RE = re.compile(r"[a-z][a-z0-9-]*")
@@ -102,6 +104,16 @@ def packs_toml_path(root: Path) -> Path:
102
104
  return root / ".claude" / "hooks" / PACKS_TOML
103
105
 
104
106
 
107
+ def manifest_in(root: Path) -> Path:
108
+ """Return the pack manifest path under root, preferring .claude/capt-hook.toml.
109
+
110
+ Falls back to the repo-root location. The returned path may not exist (the
111
+ canonical missing location), so PackManifest.load still fails loudly.
112
+ """
113
+ claude = root / ".claude" / PACK_MANIFEST
114
+ return claude if claude.is_file() else root / PACK_MANIFEST
115
+
116
+
105
117
  def parse_entry(name: str, table: dict[str, Any]) -> PackEntry:
106
118
  match table:
107
119
  case {"source": source, "commit": commit}:
@@ -175,17 +187,18 @@ def strip_top_level(tf: tarfile.TarFile) -> Iterator[tarfile.TarInfo]:
175
187
  yield member
176
188
 
177
189
 
178
- def members_under(members: list[tarfile.TarInfo], hooks: str) -> Iterator[tarfile.TarInfo]:
190
+ def members_under(members: list[tarfile.TarInfo], hooks: str, manifest_path: str) -> Iterator[tarfile.TarInfo]:
179
191
  """Yield the manifest plus members within the pack's hooks dir.
180
192
 
181
193
  hooks == "." (hooks beside the manifest) selects the whole tree; a real
182
194
  subdir selects only the manifest and that subtree, so the cache holds just
183
- what the loader imports.
195
+ what the loader imports. The manifest is included by its actual archive path
196
+ so a .claude/ manifest survives without dragging in the rest of .claude/.
184
197
  """
185
198
  rel = hooks.strip("/")
186
199
  prefix = "" if rel in ("", ".") else rel + "/"
187
200
  for m in members:
188
- if m.path == PACK_MANIFEST or not prefix or m.path == rel or m.path.startswith(prefix):
201
+ if m.path == manifest_path or not prefix or m.path == rel or m.path.startswith(prefix):
189
202
  yield m
190
203
 
191
204
 
@@ -203,12 +216,15 @@ def fetch_commit(source: PackSource, sha: str) -> ResolvedPack:
203
216
  shutil.rmtree(staging)
204
217
  with tarfile.open(tarball) as tf:
205
218
  members = list(strip_top_level(tf))
206
- manifest_member = next((m for m in members if m.path == PACK_MANIFEST), None)
219
+ by_path = {m.path: m for m in members}
220
+ manifest_member = next((by_path[p] for p in MANIFEST_MEMBERS if p in by_path), None)
207
221
  if manifest_member is None:
208
222
  raise PackError(f"pack manifest {PACK_MANIFEST} missing in {source}")
209
223
  tf.extract(manifest_member, staging, filter="data")
210
- manifest = PackManifest.load(staging / PACK_MANIFEST)
211
- tf.extractall(staging, members=list(members_under(members, manifest.hooks)), filter="data")
224
+ manifest = PackManifest.load(staging / manifest_member.path)
225
+ tf.extractall(
226
+ staging, members=list(members_under(members, manifest.hooks, manifest_member.path)), filter="data"
227
+ )
212
228
  final = root / f"{manifest.name}@{sha}"
213
229
  if final.exists():
214
230
  shutil.rmtree(final)
@@ -225,20 +241,20 @@ def fetch_pack(source: PackSource) -> ResolvedPack:
225
241
 
226
242
  def builtin_packs() -> dict[str, Path]:
227
243
  base = Path(str(importlib.resources.files("captain_hook") / "packs"))
228
- return {p.name: p for p in base.iterdir() if p.is_dir() and (p / PACK_MANIFEST).is_file()}
244
+ return {p.name: p for p in base.iterdir() if p.is_dir() and manifest_in(p).is_file()}
229
245
 
230
246
 
231
247
  def resolve_builtin(name: str) -> ResolvedPack:
232
248
  if not (pack_dir := builtin_packs().get(name)):
233
249
  raise PackError(f"unknown builtin pack {name!r}; available: {', '.join(sorted(builtin_packs())) or 'none'}")
234
- manifest = PackManifest.load(pack_dir / PACK_MANIFEST)
250
+ manifest = PackManifest.load(manifest_in(pack_dir))
235
251
  return ResolvedPack(BuiltinPack(name=name), manifest.hooks_dir(pack_dir), manifest)
236
252
 
237
253
 
238
254
  def resolve_external(entry: ExternalPack) -> ResolvedPack | None:
239
255
  if not (cached := find_cached(entry.name, entry.commit)):
240
256
  return None
241
- manifest = PackManifest.load(cached / PACK_MANIFEST)
257
+ manifest = PackManifest.load(manifest_in(cached))
242
258
  return ResolvedPack(entry, manifest.hooks_dir(cached), manifest)
243
259
 
244
260
 
@@ -57,13 +57,13 @@ def review_wired(hooks: dict[str, Any]) -> bool:
57
57
 
58
58
 
59
59
  def ensure_review_wiring(settings_path: Path) -> bool:
60
- from captain_hook.cli import write_settings
60
+ from captain_hook.cli import sibling_settings, write_settings
61
61
 
62
62
  existing: dict[str, Any] = json.loads(settings_path.read_text()) if settings_path.exists() else {}
63
- committed = settings_path.parent / "settings.json"
64
- committed_hooks: dict[str, Any] = (json.loads(committed.read_text()).get("hooks") or {}) if committed.exists() else {}
63
+ sibling = sibling_settings(settings_path)
64
+ sibling_hooks: dict[str, Any] = (json.loads(sibling.read_text()).get("hooks") or {}) if sibling.exists() else {}
65
65
  hooks: dict[str, Any] = existing.get("hooks") or {}
66
- if review_wired(hooks) or review_wired(committed_hooks):
66
+ if review_wired(hooks) or review_wired(sibling_hooks):
67
67
  return False
68
68
  group = {"hooks": [{"type": "command", "command": f"uvx {REVIEW_RUN_COMMAND}"}]}
69
69
  write_settings(
@@ -126,8 +126,8 @@ def enable(state: CliState) -> None:
126
126
  repo = current_repo(state.root)
127
127
  watch_repo(repo)
128
128
  register_marketplace(state.root)
129
- wired = ensure_review_wiring(state.root / ".claude" / "settings.local.json")
130
- click.echo(f"watching {repo}" + (" (SessionEnd hook wired into .claude/settings.local.json)" if wired else ""))
129
+ wired = ensure_review_wiring(state.root / ".claude" / "settings.json")
130
+ click.echo(f"watching {repo}" + (" (SessionEnd hook wired into .claude/settings.json)" if wired else ""))
131
131
 
132
132
 
133
133
  @review.command()
@@ -9,7 +9,7 @@ allowed-tools: Read, Grep, Glob, Write, Edit, Bash(uvx capt-hook:*, capt-hook:*,
9
9
 
10
10
  capt-hook is a declarative hook framework for Claude Code. Hooks are Python files in
11
11
  `.claude/hooks/`, dispatched by `uvx capt-hook run <Event>` entries in
12
- `.claude/settings.local.json`. Each hook carries inline tests —
12
+ `.claude/settings.json`. Each hook carries inline tests —
13
13
  `tests={Input(...): Block() | Warn() | Allow()}` — run with `uvx capt-hook test`. This
14
14
  skill turns **one durable correction** (the user's verbatim feedback plus the context it
15
15
  fired in) into **one new hook file** `.claude/hooks/<slug>.py`. Full API:
@@ -111,7 +111,7 @@ hook.
111
111
  ### 5. Wire settings
112
112
 
113
113
  Only after Step 4 is green, and only when the hook targets an event no existing
114
- `.claude/settings.local.json` entry dispatches:
114
+ `.claude/settings.json` entry dispatches:
115
115
 
116
116
  ```bash
117
117
  uvx capt-hook register-hooks
@@ -159,7 +159,7 @@ Glob caveat: patterns match the full relative path. `**/*.py` matches `src/main.
159
159
  |---|---|
160
160
  | `uvx capt-hook init` | Scaffold `.claude/hooks/example.py` + merge settings entries |
161
161
  | `uvx capt-hook test [--json]` | Run all inline tests; exit 1 on failure; `--json` = one record per test |
162
- | `uvx capt-hook register-hooks [--hooks-dir D] [--dry-run] [--from SRC]` | Merge captain-hook's hooks into `.claude/settings.local.json` and write it (`--dry-run` prints without writing) |
162
+ | `uvx capt-hook register-hooks [--hooks-dir D] [--dry-run] [--from SRC]` | Merge captain-hook's hooks into `.claude/settings.json` and write it (`--dry-run` prints without writing) |
163
163
  | `uvx capt-hook run <Event> [--async]` | Dispatch one event (Claude Code calls this, not you) |
164
164
  | `uvx capt-hook logs [--session S] [--tail N]` | View a recent capt-hook session log |
165
165
 
@@ -44,7 +44,7 @@ broken hook.
44
44
 
45
45
  Wire only commands proven to run: execute the exact settings command by hand first, and
46
46
  prefer `uvx capt-hook register-hooks` (which writes known-good entries) over editing
47
- `.claude/settings.local.json` manually.
47
+ `.claude/settings.json` manually.
48
48
 
49
49
  ## 4. `uvx capt-hook test` green BEFORE wiring — always
50
50
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: bootstrapping-hooks
3
- description: Surveys a repository and sets up captain-hook (capt-hook) guardrails for Claude Code — blocking gates, advisory nudges, command blocks, and test-integrity checks mined from the repo's own docs, CI workflows, lint configs, and git history. Scaffolds the framework and enables the session reviewer up front (Step 1), then proposes categorized candidates for user approval before writing anything, then writes .claude/hooks/*.py with inline tests, verifies with capt-hook test, and wires .claude/settings.local.json. Use when the user asks to "set up captain hook", "set up capt-hook", "set up hooks", "bootstrap capt-hook", "add guardrails", "enforce our conventions with hooks", "protect this repo", or "make Claude follow CONTRIBUTING.md".
3
+ description: Surveys a repository and sets up captain-hook (capt-hook) guardrails for Claude Code — blocking gates, advisory nudges, command blocks, and test-integrity checks mined from the repo's own docs, CI workflows, lint configs, and git history. Scaffolds the framework and enables the session reviewer up front (Step 1), then proposes categorized candidates for user approval before writing anything, then writes .claude/hooks/*.py with inline tests, verifies with capt-hook test, and wires .claude/settings.json. Use when the user asks to "set up captain hook", "set up capt-hook", "set up hooks", "bootstrap capt-hook", "add guardrails", "enforce our conventions with hooks", "protect this repo", or "make Claude follow CONTRIBUTING.md".
4
4
  argument-hint: "[repo path] (defaults to current project)"
5
5
  allowed-tools: Read, Grep, Glob, AskUserQuestion, Write, Edit, Bash(uvx capt-hook:*, capt-hook:*, git log:*, git diff:*, ls:*, find:*)
6
6
  ---
@@ -9,7 +9,7 @@ allowed-tools: Read, Grep, Glob, AskUserQuestion, Write, Edit, Bash(uvx capt-hoo
9
9
 
10
10
  capt-hook is a declarative hook framework for Claude Code. Hooks are Python files in
11
11
  `.claude/hooks/`, dispatched by `uvx capt-hook run <Event>` entries in
12
- `.claude/settings.local.json`. Each hook carries inline tests —
12
+ `.claude/settings.json`. Each hook carries inline tests —
13
13
  `tests={Input(...): Block() | Warn() | Allow()}` — run with `uvx capt-hook test`. Hooks are
14
14
  always Python regardless of the target repo's language: conditions like `Command` and
15
15
  `FilePath` are language-agnostic; only AST `lint` rules are Python-specific. The full
@@ -51,11 +51,11 @@ grep -lq 'capt-hook' .claude/settings.json 2>/dev/null && echo COMMITTED || echo
51
51
 
52
52
  Then scaffold up front, so the framework and the session reviewer are live before you propose
53
53
  anything. Run `uvx capt-hook init` in every repo. It scaffolds `.claude/hooks/`,
54
- wires `.claude/settings.local.json`, installs the skills, and **enables the session reviewer**
54
+ wires `.claude/settings.json`, installs the skills, and **enables the session reviewer**
55
55
  (watching this repo; it mines ended sessions and opens hook PRs — `uvx capt-hook review disable`
56
- to stop). In a **COMMITTED** repo (a checked-in `.claude/settings.json` already runs
57
- `uvx capt-hook run …`), `init` defers those events to the committed file instead of re-wiring
58
- them locally. It prints `deferred to settings.json: …` and never double-fires.
56
+ to stop). When `.claude/settings.local.json` already runs `uvx capt-hook run …` for some events
57
+ (a per-machine setup), `init` defers those events to the local file instead of duplicating them
58
+ into the committed settings. It prints `deferred to settings.local.json: …` and never double-fires.
59
59
 
60
60
  Read `.claude/settings.local.json` and `.claude/settings.json`. If capt-hook hooks already exist,
61
61
  switch to **additive mode**: never overwrite existing hook files; new categories go in new files,
@@ -154,7 +154,7 @@ after scaffolding). Run:
154
154
  uvx capt-hook register-hooks
155
155
  ```
156
156
 
157
- `register-hooks` writes `.claude/settings.local.json` directly, merging non-destructively: it
157
+ `register-hooks` writes `.claude/settings.json` directly, merging non-destructively: it
158
158
  preserves every non-captain-hook entry, refreshes captain-hook's own, and drops entries for
159
159
  events you no longer subscribe to. Add `--dry-run` to print the merged JSON without writing.
160
160
 
@@ -25,7 +25,7 @@ defaults to the process cwd. This is what spawned you; do not recurse into it.
25
25
  ### `review enable` / `review disable`
26
26
 
27
27
  `enable` marks the current repo watched and wires the SessionEnd hook into
28
- `.claude/settings.local.json` (idempotent). `disable` stops watching; candidates stay
28
+ `.claude/settings.json` (idempotent). `disable` stops watching; candidates stay
29
29
  recorded but never become eligible.
30
30
 
31
31
  ### `review scan [--transcript <file>]... [--dir <dir>]...`
@@ -166,7 +166,7 @@ and every `Input` runs through the *whole* styleguide. A failing test usually me
166
166
  input trips a sibling rule — shrink it to a single construct that trips exactly one rule.
167
167
 
168
168
  If `style_llm.py` added hooks on new events (e.g. a `Stop`-targeted `llm_nudge`), run
169
- `uvx capt-hook register-hooks` (it merges non-destructively into `.claude/settings.local.json`
169
+ `uvx capt-hook register-hooks` (it merges non-destructively into `.claude/settings.json`
170
170
  and writes it).
171
171
 
172
172
  ### 8. Enforcement report
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "capt-hook"
3
- version = "3.5.0"
3
+ version = "3.7.0"
4
4
  description = "Declarative hook framework for Claude Code"
5
5
  readme = "README.md"
6
6
  license = "PolyForm-Noncommercial-1.0.0"
File without changes
File without changes
File without changes
File without changes