capt-hook 6.4.0__tar.gz → 6.5.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.
- capt_hook-6.5.0/PKG-INFO +146 -0
- capt_hook-6.5.0/README.md +98 -0
- capt_hook-6.5.0/captain_hook/packs/general/capt-hook.toml +5 -0
- capt_hook-6.5.0/captain_hook/packs/general/detours.py +151 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/pyproject.toml +2 -2
- capt_hook-6.4.0/PKG-INFO +0 -137
- capt_hook-6.4.0/README.md +0 -89
- capt_hook-6.4.0/captain_hook/packs/general/capt-hook.toml +0 -5
- {capt_hook-6.4.0 → capt_hook-6.5.0}/LICENSE +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/.claude-plugin/plugin.json +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/__main__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/app.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/ast_grep.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/cli.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/command.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/conditions.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/context.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/contexts.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/decisions.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/dispatch.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/durable.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/events.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/fields.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/file.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/loader.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/log.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/general/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/general/_lib.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/general/commands.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/general/docs.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/general/models.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/general/plans.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/general/prompts.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/general/review.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/general/tasks.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/general/tombstones.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/go/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/go/capt-hook.toml +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/go/testing.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/go/toolchain.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/manager.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/python/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/python/capt-hook.toml +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/python/style.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/python/testing.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/python/toolchain.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/steering/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/steering/capt-hook.toml +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/packs/steering/steering.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/prompt.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/py.typed +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/cli.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/fix.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/formats.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/judge.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/repo.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/scan.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/settings.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/store.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/review/sync.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/session.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/settings.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/state.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/style/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/style/matchers.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/style/scope.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/style/types.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/tasks.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/testing/types.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/types.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/util/__init__.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/util/fs.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/util/http.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/util/text.py +0 -0
capt_hook-6.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: capt-hook
|
|
3
|
+
Version: 6.5.0
|
|
4
|
+
Summary: Stop repeating yourself to Claude.
|
|
5
|
+
Keywords: claude,claude-code,hooks,llm,agents,guardrails,cli
|
|
6
|
+
Author: Yasyf Mohamedali
|
|
7
|
+
Author-email: Yasyf Mohamedali <yasyfm@gmail.com>
|
|
8
|
+
License-Expression: PolyForm-Noncommercial-1.0.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: cc-transcript>=7.1,<8
|
|
20
|
+
Requires-Dist: pydantic>=2.0
|
|
21
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
22
|
+
Requires-Dist: tree-sitter>=0.24
|
|
23
|
+
Requires-Dist: tree-sitter-bash>=0.23
|
|
24
|
+
Requires-Dist: ast-grep-py>=0.39,<1
|
|
25
|
+
Requires-Dist: funcy>=2.0
|
|
26
|
+
Requires-Dist: spacy>=3.7
|
|
27
|
+
Requires-Dist: click>=8
|
|
28
|
+
Requires-Dist: rich>=13
|
|
29
|
+
Requires-Dist: orjsonl>=1.0
|
|
30
|
+
Requires-Dist: wn>=1.1.0
|
|
31
|
+
Requires-Dist: lazy-object-proxy>=1.12.0
|
|
32
|
+
Requires-Dist: filelock>=3
|
|
33
|
+
Requires-Dist: loguru>=0.7.3
|
|
34
|
+
Requires-Dist: spawnllm>=0.5.2,<0.6
|
|
35
|
+
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
|
|
37
|
+
Requires-Dist: pyright>=1.1 ; extra == 'dev'
|
|
38
|
+
Requires-Dist: pyyaml>=6 ; extra == 'dev'
|
|
39
|
+
Requires-Dist: ruff>=0.8 ; extra == 'dev'
|
|
40
|
+
Requires-Python: >=3.13
|
|
41
|
+
Project-URL: Homepage, https://github.com/yasyf/captain-hook
|
|
42
|
+
Project-URL: Documentation, https://yasyf.github.io/captain-hook/
|
|
43
|
+
Project-URL: Repository, https://github.com/yasyf/captain-hook
|
|
44
|
+
Project-URL: Issues, https://github.com/yasyf/captain-hook/issues
|
|
45
|
+
Project-URL: Changelog, https://github.com/yasyf/captain-hook/blob/main/CHANGELOG.md
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
|
|
49
|
+
# 
|
|
50
|
+
|
|
51
|
+
**Stop repeating yourself to Claude.** captain-hook mines your transcripts for the corrections you keep giving and opens PRs that turn each one into a typed, tested Python hook.
|
|
52
|
+
|
|
53
|
+
[](https://github.com/yasyf/captain-hook/actions/workflows/ci.yml)
|
|
54
|
+
[](https://pypi.org/project/capt-hook/)
|
|
55
|
+
[](https://github.com/yasyf/captain-hook/blob/main/LICENSE)
|
|
56
|
+
|
|
57
|
+
## Get started
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uvx capt-hook init
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`init` scaffolds `.claude/hooks/`, wires Claude Code's settings, and arms the session reviewer. One `block_command` later, a force-push dies at `PreToolUse` and the hook's inline tests run green:
|
|
64
|
+
|
|
65
|
+
<img src="https://github.com/yasyf/captain-hook/raw/main/docs/assets/demo.png" alt="Terminal running 'uvx capt-hook test' — a hook blocks git push --force and both inline tests pass" width="700">
|
|
66
|
+
|
|
67
|
+
Driving with an agent? Paste this:
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
/plugin marketplace add yasyf/captain-hook
|
|
71
|
+
/plugin install captain-hook@captain-hook
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
<details>
|
|
75
|
+
<summary>Prefer a prompt over the plugin?</summary>
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
Run `uvx capt-hook init` in this repo, write one hook that blocks force-pushes,
|
|
79
|
+
and verify it with `uvx capt-hook test`. Read https://yasyf.github.io/captain-hook/
|
|
80
|
+
if you get stuck.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
</details>
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Use cases
|
|
88
|
+
|
|
89
|
+
### Block force-push and rm -rf before they run
|
|
90
|
+
|
|
91
|
+
One bad Bash call rewrites shared history or eats a directory, and by the time you spot it in the transcript it already ran. Declare the block once, tests inline:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
# .claude/hooks/safety.py
|
|
95
|
+
from captain_hook import Allow, Block, Input, block_command
|
|
96
|
+
|
|
97
|
+
block_command(
|
|
98
|
+
["git", "push", "--force"],
|
|
99
|
+
reason="Force-pushing rewrites shared history",
|
|
100
|
+
hint="Use `git push --force-with-lease` instead",
|
|
101
|
+
tests={
|
|
102
|
+
Input(command="git push --force"): Block(),
|
|
103
|
+
Input(command="git push origin main"): Allow(),
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The next `git push --force` never executes: the agent sees `BLOCKED: Force-pushing rewrites shared history` plus the hint, and reaches for `--force-with-lease` instead. `rm -rf` is one more `block_command` away.
|
|
109
|
+
|
|
110
|
+
### Turn repeated corrections into rules Claude can't forget
|
|
111
|
+
|
|
112
|
+
You've typed "use uv, not pip" in a dozen sessions, and session thirteen makes the same mistake. After `init`, the session reviewer reads each transcript as the session ends, keeps the corrections that are standing rules, and — once a pattern proves itself across sessions — opens a PR that codifies it as a hook. Watch the pipeline:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
uvx capt-hook status
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The dashboard lists every correction it's tracking, staged from first sighting to open PR. You review the PR like any other; merged hooks enforce the rule from then on.
|
|
119
|
+
|
|
120
|
+
### Gate "done" until the tests actually pass
|
|
121
|
+
|
|
122
|
+
The agent declares victory while the suite is red. A Stop gate holds the line:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# .claude/hooks/quality.py
|
|
126
|
+
from captain_hook import RanCommand, TouchedFile, gate
|
|
127
|
+
|
|
128
|
+
gate(
|
|
129
|
+
"You edited Python files but never ran the tests. Run `uv run pytest` before finishing.",
|
|
130
|
+
only_if=[TouchedFile("**/*.py")],
|
|
131
|
+
skip_if=[RanCommand(r"\bpytest\b")],
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The agent can't end the turn until a pytest run shows up in the transcript, and the gate stands down on its own once one does.
|
|
136
|
+
|
|
137
|
+
## More in the docs
|
|
138
|
+
|
|
139
|
+
- **Session reviewer** — the full corrections lifecycle, from transcript to merged hook PR — [guide](https://yasyf.github.io/captain-hook/docs/guide/session-reviewer.html)
|
|
140
|
+
- **Conditions** — typed filters over tools, files, commands, and transcript history — [guide](https://yasyf.github.io/captain-hook/docs/guide/conditions.html)
|
|
141
|
+
- **LLM hooks** — gate on a model's verdict when a regex can't decide — [guide](https://yasyf.github.io/captain-hook/docs/guide/llm-hooks.html)
|
|
142
|
+
- **Workflows** — multi-step Stop gates with artifact checks and checklists — [guide](https://yasyf.github.io/captain-hook/docs/guide/workflows.html)
|
|
143
|
+
- **Packs** — the shipped `general`, `python`, and `go` hook packs — [guide](https://yasyf.github.io/captain-hook/docs/guide/packs.html)
|
|
144
|
+
- **Testing** — run `uvx capt-hook test --json` in CI so a regressed hook fails the build — [guide](https://yasyf.github.io/captain-hook/docs/guide/testing.html)
|
|
145
|
+
|
|
146
|
+
Read the [docs](https://yasyf.github.io/captain-hook/) for the full guide. Licensed under [PolyForm Noncommercial 1.0.0](https://github.com/yasyf/captain-hook/blob/main/LICENSE), free for noncommercial use.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# 
|
|
2
|
+
|
|
3
|
+
**Stop repeating yourself to Claude.** captain-hook mines your transcripts for the corrections you keep giving and opens PRs that turn each one into a typed, tested Python hook.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/yasyf/captain-hook/actions/workflows/ci.yml)
|
|
6
|
+
[](https://pypi.org/project/capt-hook/)
|
|
7
|
+
[](https://github.com/yasyf/captain-hook/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
## Get started
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uvx capt-hook init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`init` scaffolds `.claude/hooks/`, wires Claude Code's settings, and arms the session reviewer. One `block_command` later, a force-push dies at `PreToolUse` and the hook's inline tests run green:
|
|
16
|
+
|
|
17
|
+
<img src="https://github.com/yasyf/captain-hook/raw/main/docs/assets/demo.png" alt="Terminal running 'uvx capt-hook test' — a hook blocks git push --force and both inline tests pass" width="700">
|
|
18
|
+
|
|
19
|
+
Driving with an agent? Paste this:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
/plugin marketplace add yasyf/captain-hook
|
|
23
|
+
/plugin install captain-hook@captain-hook
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
<details>
|
|
27
|
+
<summary>Prefer a prompt over the plugin?</summary>
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
Run `uvx capt-hook init` in this repo, write one hook that blocks force-pushes,
|
|
31
|
+
and verify it with `uvx capt-hook test`. Read https://yasyf.github.io/captain-hook/
|
|
32
|
+
if you get stuck.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
</details>
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Use cases
|
|
40
|
+
|
|
41
|
+
### Block force-push and rm -rf before they run
|
|
42
|
+
|
|
43
|
+
One bad Bash call rewrites shared history or eats a directory, and by the time you spot it in the transcript it already ran. Declare the block once, tests inline:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# .claude/hooks/safety.py
|
|
47
|
+
from captain_hook import Allow, Block, Input, block_command
|
|
48
|
+
|
|
49
|
+
block_command(
|
|
50
|
+
["git", "push", "--force"],
|
|
51
|
+
reason="Force-pushing rewrites shared history",
|
|
52
|
+
hint="Use `git push --force-with-lease` instead",
|
|
53
|
+
tests={
|
|
54
|
+
Input(command="git push --force"): Block(),
|
|
55
|
+
Input(command="git push origin main"): Allow(),
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The next `git push --force` never executes: the agent sees `BLOCKED: Force-pushing rewrites shared history` plus the hint, and reaches for `--force-with-lease` instead. `rm -rf` is one more `block_command` away.
|
|
61
|
+
|
|
62
|
+
### Turn repeated corrections into rules Claude can't forget
|
|
63
|
+
|
|
64
|
+
You've typed "use uv, not pip" in a dozen sessions, and session thirteen makes the same mistake. After `init`, the session reviewer reads each transcript as the session ends, keeps the corrections that are standing rules, and — once a pattern proves itself across sessions — opens a PR that codifies it as a hook. Watch the pipeline:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uvx capt-hook status
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The dashboard lists every correction it's tracking, staged from first sighting to open PR. You review the PR like any other; merged hooks enforce the rule from then on.
|
|
71
|
+
|
|
72
|
+
### Gate "done" until the tests actually pass
|
|
73
|
+
|
|
74
|
+
The agent declares victory while the suite is red. A Stop gate holds the line:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# .claude/hooks/quality.py
|
|
78
|
+
from captain_hook import RanCommand, TouchedFile, gate
|
|
79
|
+
|
|
80
|
+
gate(
|
|
81
|
+
"You edited Python files but never ran the tests. Run `uv run pytest` before finishing.",
|
|
82
|
+
only_if=[TouchedFile("**/*.py")],
|
|
83
|
+
skip_if=[RanCommand(r"\bpytest\b")],
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The agent can't end the turn until a pytest run shows up in the transcript, and the gate stands down on its own once one does.
|
|
88
|
+
|
|
89
|
+
## More in the docs
|
|
90
|
+
|
|
91
|
+
- **Session reviewer** — the full corrections lifecycle, from transcript to merged hook PR — [guide](https://yasyf.github.io/captain-hook/docs/guide/session-reviewer.html)
|
|
92
|
+
- **Conditions** — typed filters over tools, files, commands, and transcript history — [guide](https://yasyf.github.io/captain-hook/docs/guide/conditions.html)
|
|
93
|
+
- **LLM hooks** — gate on a model's verdict when a regex can't decide — [guide](https://yasyf.github.io/captain-hook/docs/guide/llm-hooks.html)
|
|
94
|
+
- **Workflows** — multi-step Stop gates with artifact checks and checklists — [guide](https://yasyf.github.io/captain-hook/docs/guide/workflows.html)
|
|
95
|
+
- **Packs** — the shipped `general`, `python`, and `go` hook packs — [guide](https://yasyf.github.io/captain-hook/docs/guide/packs.html)
|
|
96
|
+
- **Testing** — run `uvx capt-hook test --json` in CI so a regressed hook fails the build — [guide](https://yasyf.github.io/captain-hook/docs/guide/testing.html)
|
|
97
|
+
|
|
98
|
+
Read the [docs](https://yasyf.github.io/captain-hook/) for the full guide. Licensed under [PolyForm Noncommercial 1.0.0](https://github.com/yasyf/captain-hook/blob/main/LICENSE), free for noncommercial use.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from captain_hook import (
|
|
4
|
+
Allow,
|
|
5
|
+
Event,
|
|
6
|
+
Input,
|
|
7
|
+
Signal,
|
|
8
|
+
Signals,
|
|
9
|
+
Tool,
|
|
10
|
+
Warn,
|
|
11
|
+
llm_nudge,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
llm_nudge(
|
|
15
|
+
"""You are a senior engineer watching another engineer ("the agent") mid-task. You are
|
|
16
|
+
running in agent mode in the project's working directory, with read tools. Your one job:
|
|
17
|
+
decide whether the agent has veered onto an UNREQUESTED DETOUR — side work nobody asked
|
|
18
|
+
for — without checking in first.
|
|
19
|
+
|
|
20
|
+
Read first, judge second:
|
|
21
|
+
- The session transcript is rendered above inside `<transcript path="...">`; long content is
|
|
22
|
+
clipped (you'll see `…(+Nch)` markers). From the full history (prefer `cc-transcript
|
|
23
|
+
show`/`grep`; else read the file) establish: (1) what was actually ASKED — the user's
|
|
24
|
+
original request plus any later redirections or standing permissions — and (2) what the
|
|
25
|
+
agent is doing RIGHT NOW — the just-run tool call and the last few assistant messages.
|
|
26
|
+
|
|
27
|
+
Discriminator: the current work is either (a) the requested task, (b) a necessary
|
|
28
|
+
prerequisite of it (the task cannot land or be verified without it), or (c) authorized —
|
|
29
|
+
the user said some form of "also…", "while you're there…", "fix anything you find", or it
|
|
30
|
+
is a small stewardship fix inside code the task already touches. Any of those -> fire=false.
|
|
31
|
+
Otherwise — the agent noticed something adjacent and started acting on it without surfacing
|
|
32
|
+
it — fire=true.
|
|
33
|
+
|
|
34
|
+
Detour tells (lean fire=true): "while I'm here" / "might as well" / "let me also" followed
|
|
35
|
+
by edits to files the task doesn't need; fixing or refactoring code the request never
|
|
36
|
+
mentioned and the task doesn't depend on; a cleanup sweep starting mid-task; chasing a side
|
|
37
|
+
mystery at length while the requested work sits unfinished.
|
|
38
|
+
|
|
39
|
+
Do NOT fire when: the side work blocks the task (a broken build or failing test the change
|
|
40
|
+
trips over); it's a small stewardship fix in a file already being edited for the task; the
|
|
41
|
+
user pre-authorized it; the agent is gathering context it needs; or the agent already
|
|
42
|
+
surfaced the discovery and offered options instead of acting.
|
|
43
|
+
|
|
44
|
+
<examples>
|
|
45
|
+
<example fire="true">
|
|
46
|
+
Asked: "rename the config flag". Agent: "While I'm here, the retry logic in client.py looks
|
|
47
|
+
wrong — let me fix that too", then edits client.py.
|
|
48
|
+
Unrequested fix in a file the rename never touches.
|
|
49
|
+
</example>
|
|
50
|
+
<example fire="true">
|
|
51
|
+
Asked: "add a --json flag". Agent: "I also noticed the error handling is inconsistent
|
|
52
|
+
across commands; I'll clean that up as well", then starts a multi-file sweep.
|
|
53
|
+
Scope expansion nobody asked for, no options offered.
|
|
54
|
+
</example>
|
|
55
|
+
<example fire="false">
|
|
56
|
+
Asked: "fix the failing test". Agent: "The fixture helper it calls has the actual bug —
|
|
57
|
+
fixing that first."
|
|
58
|
+
A prerequisite: the requested fix cannot land without it.
|
|
59
|
+
</example>
|
|
60
|
+
<example fire="false">
|
|
61
|
+
User said "clean up anything you find along the way"; agent fixes a stale docstring in a
|
|
62
|
+
file it was already editing.
|
|
63
|
+
Pre-authorized stewardship.
|
|
64
|
+
</example>
|
|
65
|
+
<example fire="false">
|
|
66
|
+
Agent: "I noticed X while working on Y — options: (1) finish Y and file X as a follow-up,
|
|
67
|
+
(2) fix X now, (3) ignore it. Which?"
|
|
68
|
+
Already surfacing options instead of acting.
|
|
69
|
+
</example>
|
|
70
|
+
</examples>
|
|
71
|
+
|
|
72
|
+
When uncertain, return fire=false. A missed detour costs one review comment; a false alarm
|
|
73
|
+
on legitimate work teaches the agent to ignore this nudge. Fire only when the current action
|
|
74
|
+
is clearly outside both the request and its prerequisites. Put your reasoning (under 50
|
|
75
|
+
words, naming the detour and the requested task) in `reasoning`.""",
|
|
76
|
+
message=lambda r: (
|
|
77
|
+
f"This looks like a detour — side work nobody asked for. {r.reasoning} "
|
|
78
|
+
"Stop and check in before acting: say what you noticed, then propose 2-4 concrete "
|
|
79
|
+
"options (finish the task and file this as a follow-up; pause and fix it now; ignore "
|
|
80
|
+
"it) and let the user pick — or, if you are a delegated agent, stop and return early "
|
|
81
|
+
"with findings plus options for your orchestrator instead of improvising. "
|
|
82
|
+
"See: AGENTS.md § Ask Before Assuming."
|
|
83
|
+
),
|
|
84
|
+
only_if=[Tool("Edit|Write|MultiEdit|NotebookEdit|Bash")],
|
|
85
|
+
events=Event.PostToolUse,
|
|
86
|
+
signals=Signals(
|
|
87
|
+
[
|
|
88
|
+
Signal(pattern=r"(?i)\bwhile (?:I'm|I am|we're|we are) (?:here|at it|in (?:here|there))\b", weight=2),
|
|
89
|
+
Signal(pattern=r"(?i)\bmight as well\b", weight=2),
|
|
90
|
+
Signal(pattern=r"(?i)\b(?:let me|I'll|I will) also\b", weight=2),
|
|
91
|
+
Signal(pattern=r"(?i)\bas a bonus\b", weight=2),
|
|
92
|
+
Signal(pattern=r"(?i)\bI (?:also )?noticed\b", weight=1),
|
|
93
|
+
Signal(pattern=r"(?i)\b(?:unrelated|a side note|tangent)\b", weight=1),
|
|
94
|
+
Signal(pattern=r"(?i)\bone more thing\b", weight=1),
|
|
95
|
+
Signal(pattern=r"(?i)\bquick(?:ly)? (?:fix|clean|tidy|refactor)\w*\b", weight=1),
|
|
96
|
+
],
|
|
97
|
+
threshold=2,
|
|
98
|
+
window=8,
|
|
99
|
+
),
|
|
100
|
+
tests={
|
|
101
|
+
Input(
|
|
102
|
+
file="client.py",
|
|
103
|
+
content="retry = 3\n",
|
|
104
|
+
transcript=[
|
|
105
|
+
{
|
|
106
|
+
"type": "assistant",
|
|
107
|
+
"message": {
|
|
108
|
+
"content": [
|
|
109
|
+
{"type": "text", "text": "While I'm here, the retry logic looks wrong — fixing it too."}
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
): Warn(pattern="detour"),
|
|
115
|
+
Input(
|
|
116
|
+
command="./scripts/cleanup.sh",
|
|
117
|
+
transcript=[
|
|
118
|
+
{
|
|
119
|
+
"type": "assistant",
|
|
120
|
+
"message": {
|
|
121
|
+
"content": [
|
|
122
|
+
{"type": "text", "text": "I also noticed stale artifacts. One more thing to clean up."}
|
|
123
|
+
]
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
): Warn(pattern="options"),
|
|
128
|
+
Input(
|
|
129
|
+
file="flags.py",
|
|
130
|
+
content="json_flag = True\n",
|
|
131
|
+
transcript=[
|
|
132
|
+
{
|
|
133
|
+
"type": "assistant",
|
|
134
|
+
"message": {"content": [{"type": "text", "text": "Implementing the requested --json flag now."}]},
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
): Allow(),
|
|
138
|
+
Input(
|
|
139
|
+
tool="Read",
|
|
140
|
+
file="client.py",
|
|
141
|
+
transcript=[
|
|
142
|
+
{
|
|
143
|
+
"type": "assistant",
|
|
144
|
+
"message": {
|
|
145
|
+
"content": [{"type": "text", "text": "While I'm here, might as well look at the retry logic."}]
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
],
|
|
149
|
+
): Allow(),
|
|
150
|
+
},
|
|
151
|
+
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "capt-hook"
|
|
3
3
|
# Inert sentinel — the real version is stamped from the release tag (uv version --frozen).
|
|
4
|
-
version = "6.
|
|
5
|
-
description = "
|
|
4
|
+
version = "6.5.0"
|
|
5
|
+
description = "Stop repeating yourself to Claude."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
license = "PolyForm-Noncommercial-1.0.0"
|
|
8
8
|
license-files = ["LICENSE"]
|
capt_hook-6.4.0/PKG-INFO
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: capt-hook
|
|
3
|
-
Version: 6.4.0
|
|
4
|
-
Summary: Declarative hook framework for Claude Code
|
|
5
|
-
Keywords: claude,claude-code,hooks,llm,agents,guardrails,cli
|
|
6
|
-
Author: Yasyf Mohamedali
|
|
7
|
-
Author-email: Yasyf Mohamedali <yasyfm@gmail.com>
|
|
8
|
-
License-Expression: PolyForm-Noncommercial-1.0.0
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Operating System :: OS Independent
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
-
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
-
Classifier: Topic :: Software Development :: Testing
|
|
18
|
-
Classifier: Typing :: Typed
|
|
19
|
-
Requires-Dist: cc-transcript>=7.1,<8
|
|
20
|
-
Requires-Dist: pydantic>=2.0
|
|
21
|
-
Requires-Dist: pydantic-settings>=2.0
|
|
22
|
-
Requires-Dist: tree-sitter>=0.24
|
|
23
|
-
Requires-Dist: tree-sitter-bash>=0.23
|
|
24
|
-
Requires-Dist: ast-grep-py>=0.39,<1
|
|
25
|
-
Requires-Dist: funcy>=2.0
|
|
26
|
-
Requires-Dist: spacy>=3.7
|
|
27
|
-
Requires-Dist: click>=8
|
|
28
|
-
Requires-Dist: rich>=13
|
|
29
|
-
Requires-Dist: orjsonl>=1.0
|
|
30
|
-
Requires-Dist: wn>=1.1.0
|
|
31
|
-
Requires-Dist: lazy-object-proxy>=1.12.0
|
|
32
|
-
Requires-Dist: filelock>=3
|
|
33
|
-
Requires-Dist: loguru>=0.7.3
|
|
34
|
-
Requires-Dist: spawnllm>=0.5.2,<0.6
|
|
35
|
-
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
36
|
-
Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
|
|
37
|
-
Requires-Dist: pyright>=1.1 ; extra == 'dev'
|
|
38
|
-
Requires-Dist: pyyaml>=6 ; extra == 'dev'
|
|
39
|
-
Requires-Dist: ruff>=0.8 ; extra == 'dev'
|
|
40
|
-
Requires-Python: >=3.13
|
|
41
|
-
Project-URL: Homepage, https://github.com/yasyf/captain-hook
|
|
42
|
-
Project-URL: Documentation, https://yasyf.github.io/captain-hook/
|
|
43
|
-
Project-URL: Repository, https://github.com/yasyf/captain-hook
|
|
44
|
-
Project-URL: Issues, https://github.com/yasyf/captain-hook/issues
|
|
45
|
-
Project-URL: Changelog, https://github.com/yasyf/captain-hook/blob/main/CHANGELOG.md
|
|
46
|
-
Provides-Extra: dev
|
|
47
|
-
Description-Content-Type: text/markdown
|
|
48
|
-
|
|
49
|
-
# captain-hook
|
|
50
|
-
|
|
51
|
-

|
|
52
|
-
|
|
53
|
-
[](https://pypi.org/project/capt-hook/)
|
|
54
|
-
[](https://pypi.org/project/capt-hook/)
|
|
55
|
-
[](https://yasyf.github.io/captain-hook/)
|
|
56
|
-
[](https://github.com/yasyf/captain-hook/blob/main/LICENSE)
|
|
57
|
-
|
|
58
|
-
Guardrails for Claude Code, written as typed, testable data — and learned from the corrections you give Claude.
|
|
59
|
-
|
|
60
|
-
A captain-hook hook is declarative Python: an event, some conditions, an action. Block a footgun before it runs, nudge the agent off a bad pattern, gate "done" until the tests pass. Then captain-hook closes the loop: it reads the corrections you give Claude as you work and opens pull requests that codify the durable ones as new hooks. You write the first few; it writes the rest.
|
|
61
|
-
|
|
62
|
-
## Install
|
|
63
|
-
|
|
64
|
-
captain-hook needs no install — it runs through [uvx](https://docs.astral.sh/uv/). From your project root:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
uvx capt-hook init
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
`init` scaffolds `.claude/hooks/`, wires Claude Code's settings, registers the captain-hook plugin so its skills install on workspace-trust, and arms the [session reviewer](#it-learns-from-your-corrections). Or do it all from a session. Run `/plugin marketplace add yasyf/captain-hook`, then ask Claude to "set up captain hook".
|
|
71
|
-
|
|
72
|
-
## Your first hook
|
|
73
|
-
|
|
74
|
-
A hook is an event, some conditions, and an action. This one stops the agent from finishing a UI change it never looked at:
|
|
75
|
-
|
|
76
|
-
```python
|
|
77
|
-
# .claude/hooks/visual_review.py
|
|
78
|
-
from captain_hook import gate, TouchedFile, UsedSkill
|
|
79
|
-
|
|
80
|
-
gate(
|
|
81
|
-
"You edited UI files. Open them with agent-browser and verify they render before finishing.",
|
|
82
|
-
only_if=[TouchedFile("**/src/routes/**", "**/src/components/**")],
|
|
83
|
-
skip_if=[UsedSkill("agent-browser")],
|
|
84
|
-
)
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
`only_if` arms the gate only when UI files changed; `skip_if` stands it down once the agent has done the review. Conditions match tools, files, commands, and even which skills the agent used.
|
|
88
|
-
|
|
89
|
-
## It learns from your corrections
|
|
90
|
-
|
|
91
|
-
Most hooks you'll never write by hand.
|
|
92
|
-
|
|
93
|
-
The corrections you give Claude as you work are exactly the rules a hook should enforce: "never force-push", "use `uv`, not `pip`", "you weakened that test". Writing the hook by hand is friction you skip in the moment, so the **session reviewer** notices for you. When a session ends, it reads the transcript, finds the durable corrections and the hooks that misfired, judges which ones are standing rules and which are one-offs, and once a pattern proves itself across sessions, opens a pull request that adds the hook — or fixes the one that misfired. You review the PR like any other.
|
|
94
|
-
|
|
95
|
-
It's on by default after `init`. Turn it off for a repo with `uvx capt-hook review disable`. The [session reviewer guide](https://yasyf.github.io/captain-hook/docs/guide/session-reviewer.html) covers the prerequisites (an authenticated `claude` and `gh`) and the `HOOKS_REVIEW_*` thresholds.
|
|
96
|
-
|
|
97
|
-
## Tested like code
|
|
98
|
-
|
|
99
|
-
Every deterministic hook carries inline tests, so a broken hook fails like broken code:
|
|
100
|
-
|
|
101
|
-
```python
|
|
102
|
-
# .claude/hooks/safety.py
|
|
103
|
-
from captain_hook import Allow, Block, Input, block_command
|
|
104
|
-
|
|
105
|
-
block_command(
|
|
106
|
-
["git", "stash"],
|
|
107
|
-
reason="Use the team's VCS workflow for shelving changes",
|
|
108
|
-
hint="Commit a WIP change instead of stashing",
|
|
109
|
-
tests={
|
|
110
|
-
Input(command="git stash"): Block(),
|
|
111
|
-
Input(command="git status"): Allow(),
|
|
112
|
-
},
|
|
113
|
-
)
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Run them from your project root, where `--hooks` defaults to `.claude/hooks`:
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
uvx capt-hook test
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Wire that into CI and you catch a broken hook the way you catch broken code.
|
|
123
|
-
|
|
124
|
-
## What it's for
|
|
125
|
-
|
|
126
|
-
- Block footguns before they run on `PreToolUse`: force-push, `rm -rf`, package-manager traps.
|
|
127
|
-
- Steer the agent with feedback that fires on the patterns it actually emits: repeated failures, weakened tests, missed conventions.
|
|
128
|
-
- Hold the line on multi-step work with Stop gates and artifact checks, so the agent can't call it "done" before the tests run or the report's written.
|
|
129
|
-
- Keep all of it testable; every hook ships with inline tests that run in CI.
|
|
130
|
-
|
|
131
|
-
## Docs
|
|
132
|
-
|
|
133
|
-
[Read the docs](https://yasyf.github.io/captain-hook/) for the full guide to conditions, primitives, LLM hooks, workflows, state, and real-world patterns. To work on captain-hook itself, see the [development guide](https://yasyf.github.io/captain-hook/docs/development/).
|
|
134
|
-
|
|
135
|
-
## License
|
|
136
|
-
|
|
137
|
-
Licensed under [PolyForm Noncommercial 1.0.0](LICENSE), free for noncommercial use.
|
capt_hook-6.4.0/README.md
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# captain-hook
|
|
2
|
-
|
|
3
|
-

|
|
4
|
-
|
|
5
|
-
[](https://pypi.org/project/capt-hook/)
|
|
6
|
-
[](https://pypi.org/project/capt-hook/)
|
|
7
|
-
[](https://yasyf.github.io/captain-hook/)
|
|
8
|
-
[](https://github.com/yasyf/captain-hook/blob/main/LICENSE)
|
|
9
|
-
|
|
10
|
-
Guardrails for Claude Code, written as typed, testable data — and learned from the corrections you give Claude.
|
|
11
|
-
|
|
12
|
-
A captain-hook hook is declarative Python: an event, some conditions, an action. Block a footgun before it runs, nudge the agent off a bad pattern, gate "done" until the tests pass. Then captain-hook closes the loop: it reads the corrections you give Claude as you work and opens pull requests that codify the durable ones as new hooks. You write the first few; it writes the rest.
|
|
13
|
-
|
|
14
|
-
## Install
|
|
15
|
-
|
|
16
|
-
captain-hook needs no install — it runs through [uvx](https://docs.astral.sh/uv/). From your project root:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
uvx capt-hook init
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
`init` scaffolds `.claude/hooks/`, wires Claude Code's settings, registers the captain-hook plugin so its skills install on workspace-trust, and arms the [session reviewer](#it-learns-from-your-corrections). Or do it all from a session. Run `/plugin marketplace add yasyf/captain-hook`, then ask Claude to "set up captain hook".
|
|
23
|
-
|
|
24
|
-
## Your first hook
|
|
25
|
-
|
|
26
|
-
A hook is an event, some conditions, and an action. This one stops the agent from finishing a UI change it never looked at:
|
|
27
|
-
|
|
28
|
-
```python
|
|
29
|
-
# .claude/hooks/visual_review.py
|
|
30
|
-
from captain_hook import gate, TouchedFile, UsedSkill
|
|
31
|
-
|
|
32
|
-
gate(
|
|
33
|
-
"You edited UI files. Open them with agent-browser and verify they render before finishing.",
|
|
34
|
-
only_if=[TouchedFile("**/src/routes/**", "**/src/components/**")],
|
|
35
|
-
skip_if=[UsedSkill("agent-browser")],
|
|
36
|
-
)
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
`only_if` arms the gate only when UI files changed; `skip_if` stands it down once the agent has done the review. Conditions match tools, files, commands, and even which skills the agent used.
|
|
40
|
-
|
|
41
|
-
## It learns from your corrections
|
|
42
|
-
|
|
43
|
-
Most hooks you'll never write by hand.
|
|
44
|
-
|
|
45
|
-
The corrections you give Claude as you work are exactly the rules a hook should enforce: "never force-push", "use `uv`, not `pip`", "you weakened that test". Writing the hook by hand is friction you skip in the moment, so the **session reviewer** notices for you. When a session ends, it reads the transcript, finds the durable corrections and the hooks that misfired, judges which ones are standing rules and which are one-offs, and once a pattern proves itself across sessions, opens a pull request that adds the hook — or fixes the one that misfired. You review the PR like any other.
|
|
46
|
-
|
|
47
|
-
It's on by default after `init`. Turn it off for a repo with `uvx capt-hook review disable`. The [session reviewer guide](https://yasyf.github.io/captain-hook/docs/guide/session-reviewer.html) covers the prerequisites (an authenticated `claude` and `gh`) and the `HOOKS_REVIEW_*` thresholds.
|
|
48
|
-
|
|
49
|
-
## Tested like code
|
|
50
|
-
|
|
51
|
-
Every deterministic hook carries inline tests, so a broken hook fails like broken code:
|
|
52
|
-
|
|
53
|
-
```python
|
|
54
|
-
# .claude/hooks/safety.py
|
|
55
|
-
from captain_hook import Allow, Block, Input, block_command
|
|
56
|
-
|
|
57
|
-
block_command(
|
|
58
|
-
["git", "stash"],
|
|
59
|
-
reason="Use the team's VCS workflow for shelving changes",
|
|
60
|
-
hint="Commit a WIP change instead of stashing",
|
|
61
|
-
tests={
|
|
62
|
-
Input(command="git stash"): Block(),
|
|
63
|
-
Input(command="git status"): Allow(),
|
|
64
|
-
},
|
|
65
|
-
)
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Run them from your project root, where `--hooks` defaults to `.claude/hooks`:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
uvx capt-hook test
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Wire that into CI and you catch a broken hook the way you catch broken code.
|
|
75
|
-
|
|
76
|
-
## What it's for
|
|
77
|
-
|
|
78
|
-
- Block footguns before they run on `PreToolUse`: force-push, `rm -rf`, package-manager traps.
|
|
79
|
-
- Steer the agent with feedback that fires on the patterns it actually emits: repeated failures, weakened tests, missed conventions.
|
|
80
|
-
- Hold the line on multi-step work with Stop gates and artifact checks, so the agent can't call it "done" before the tests run or the report's written.
|
|
81
|
-
- Keep all of it testable; every hook ships with inline tests that run in CI.
|
|
82
|
-
|
|
83
|
-
## Docs
|
|
84
|
-
|
|
85
|
-
[Read the docs](https://yasyf.github.io/captain-hook/) for the full guide to conditions, primitives, LLM hooks, workflows, state, and real-world patterns. To work on captain-hook itself, see the [development guide](https://yasyf.github.io/captain-hook/docs/development/).
|
|
86
|
-
|
|
87
|
-
## License
|
|
88
|
-
|
|
89
|
-
Licensed under [PolyForm Noncommercial 1.0.0](LICENSE), free for noncommercial use.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md
RENAMED
|
File without changes
|
|
File without changes
|
{capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/authoring-hooks/references/pitfalls.md
RENAMED
|
File without changes
|
{capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/authoring-hooks/references/testing-hooks.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/scanning-sessions/references/pr-workflow.md
RENAMED
|
File without changes
|
{capt_hook-6.4.0 → capt_hook-6.5.0}/captain_hook/skills/scanning-sessions/references/review-cli.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|