start-vibing-stacks 2.14.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/stacks/_shared/commands/feature.md +21 -9
- package/stacks/_shared/commands/fix.md +20 -6
- package/stacks/_shared/commands/research.md +18 -7
- package/stacks/_shared/commands/validate.md +31 -4
- package/stacks/_shared/skills/codebase-knowledge/SKILL.md +46 -49
- package/stacks/_shared/skills/codebase-knowledge/TEMPLATE.md +35 -14
- package/stacks/_shared/skills/docker-patterns/SKILL.md +184 -31
- package/stacks/_shared/skills/docs-tracker/SKILL.md +83 -35
- package/stacks/_shared/skills/git-workflow/SKILL.md +140 -17
- package/stacks/_shared/skills/hook-development/SKILL.md +230 -52
- package/stacks/_shared/skills/observability/SKILL.md +72 -2
- package/stacks/_shared/skills/openapi-design/SKILL.md +111 -3
- package/stacks/_shared/skills/playwright-automation/SKILL.md +173 -27
- package/stacks/_shared/skills/postgres-patterns/SKILL.md +85 -4
- package/stacks/_shared/skills/research-cache/SKILL.md +112 -35
- package/stacks/_shared/skills/secrets-management/SKILL.md +98 -12
- package/stacks/_shared/skills/security-baseline/SKILL.md +115 -18
- package/stacks/_shared/skills/ui-ux-audit/SKILL.md +94 -35
|
@@ -1,56 +1,104 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: docs-tracker
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "File → domain mapping rules and changelog templates consumed by `documenter` v2.0.0. Detects modified files via `git diff-tree`, classifies them via `.claude/config/domain-mapping.json`, and emits the actions documenter must apply (Edit known anchors, never Write over existing files; bidirectional connections; cap commit log at 20)."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# Docs Tracker —
|
|
7
|
+
# Docs Tracker — File → Domain Mapping Rules (v2.0.0)
|
|
7
8
|
|
|
8
|
-
**ALWAYS invoke AFTER
|
|
9
|
+
**ALWAYS invoke AFTER `commit-manager` succeeds (so the commit hash is real).**
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
This skill is the **rule set** consumed by `documenter` v2.0.0. It does not write files itself — it tells documenter what to do with each changed file.
|
|
11
12
|
|
|
13
|
+
## Detection (one git call, not many)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Files in the LAST commit (after commit-manager pushed)
|
|
17
|
+
git diff-tree --no-commit-id --name-status -r HEAD
|
|
12
18
|
```
|
|
13
|
-
1. DETECT CHANGES → git diff --name-status
|
|
14
|
-
↓
|
|
15
|
-
2. CLASSIFY → A=Added, M=Modified, D=Deleted
|
|
16
|
-
↓
|
|
17
|
-
3. CHECK EXISTING DOCS → codebase-knowledge/domains/
|
|
18
|
-
↓
|
|
19
|
-
4. CREATE/UPDATE/REMOVE as needed
|
|
20
|
-
↓
|
|
21
|
-
5. UPDATE CLAUDE.md "Last Change" section
|
|
22
|
-
```
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
Status codes: `A` = added, `M` = modified, `D` = deleted, `R` = renamed, `C` = copied. Treat `R` and `C` as `M` for mapping purposes plus a "renamed from" note in the destination domain.
|
|
21
|
+
|
|
22
|
+
## File → Domain mapping
|
|
23
|
+
|
|
24
|
+
Source of truth: `.claude/config/domain-mapping.json` (shipped with this CLI; project may override). A path may map to ≥1 domain. Unmatched paths → `general`.
|
|
25
|
+
|
|
26
|
+
Skip the entire pass if every changed path matches one of:
|
|
25
27
|
|
|
26
|
-
|
|
|
28
|
+
| Skip pattern | Reason |
|
|
27
29
|
|---|---|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
30
|
+
| `.claude/**` | meta — does not belong to any product domain |
|
|
31
|
+
| `docs/**` | docs commit — no code drift |
|
|
32
|
+
| `.github/**` | CI — handled by `infrastructure` domain only if pattern is added |
|
|
33
|
+
| `CHANGELOG*`, `*.md` (root only) | release docs |
|
|
34
|
+
| `package.json`, `composer.json`, `pyproject.toml` (deps only — not scripts) | bumps tracked elsewhere |
|
|
35
|
+
|
|
36
|
+
## Action matrix (applied by `documenter`)
|
|
37
|
+
|
|
38
|
+
| Git status | Domain file state | Action |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `A` | exists | `Edit`: append row to `## Files`, prepend row to `## Recent Commits`, increment `files_count` in frontmatter |
|
|
41
|
+
| `A` | missing | `Write`: create from `TEMPLATE.md`, fill TL;DR, add the file row, add the commit row |
|
|
42
|
+
| `M` | exists | `Edit`: prepend row to `## Recent Commits`; if file's role description changed, update its row in `## Files` |
|
|
43
|
+
| `M` | missing | unusual — investigate; usually means mapping rule was added but file existed before. Treat as `A` |
|
|
44
|
+
| `D` | exists | `Edit`: strike-through (`~~path~~`) row in `## Files`, decrement `files_count`. Prune at the next pass |
|
|
45
|
+
| `R src→dst` | exists | `Edit`: replace `src` row with `dst` row, add note `(renamed from src in <short-sha>)` to Attention Points |
|
|
46
|
+
|
|
47
|
+
## Bidirectional connections (mandatory)
|
|
48
|
+
|
|
49
|
+
When a session adds `auth → api` to `auth.md`, **also** add `api ← auth` to `api.md`. The two edits must succeed atomically — if the second fails, roll back the first. The `security-auditor` flags dangling links as a MEDIUM finding.
|
|
50
|
+
|
|
51
|
+
## Cap and archive (size guard)
|
|
33
52
|
|
|
34
|
-
|
|
53
|
+
After any edit, check the live file:
|
|
35
54
|
|
|
36
55
|
```bash
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
[ "$(wc -c < domains/<slug>.md)" -gt 8192 ] && trigger_archive
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
When triggered, move the **oldest 5 rows** of `## Recent Commits` (and oldest 2 Problems & Solutions, oldest 3 Attention Points) into `<slug>.archive.md`. Live file must stay ≤ 8 KB.
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
git diff --name-only --cached
|
|
61
|
+
## Index regeneration (after every pass)
|
|
42
62
|
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
```bash
|
|
64
|
+
# Regenerate _index.json from frontmatter of every domain file
|
|
65
|
+
for f in .claude/skills/codebase-knowledge/domains/*.md; do
|
|
66
|
+
# parse YAML frontmatter, compute summary_sha = sha256(TL;DR block), emit one record
|
|
67
|
+
done | jq -s '{schema_version:1, generated_at:(now|todate), domain_count:length, domains:.}' \
|
|
68
|
+
> .claude/skills/codebase-knowledge/_index.json
|
|
45
69
|
|
|
46
|
-
#
|
|
47
|
-
|
|
70
|
+
# Then derive _INDEX.md from _index.json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`_index.json` is the source of truth — `_INDEX.md` is human-readable derivative.
|
|
74
|
+
|
|
75
|
+
## Pre-commit hook (CI integration)
|
|
76
|
+
|
|
77
|
+
A repository can add this check to fail builds when documenter forgot to run:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
HEAD_SHA=$(git rev-parse --short HEAD)
|
|
81
|
+
LAST_INDEXED=$(jq -r '.domains | map(.last_commit) | unique[]' \
|
|
82
|
+
.claude/skills/codebase-knowledge/_index.json)
|
|
83
|
+
echo "$LAST_INDEXED" | grep -qx "$HEAD_SHA" || {
|
|
84
|
+
echo "ERROR: _index.json is stale. Run documenter before pushing."
|
|
85
|
+
exit 1
|
|
86
|
+
}
|
|
48
87
|
```
|
|
49
88
|
|
|
50
89
|
## Rules
|
|
51
90
|
|
|
52
|
-
1. **
|
|
53
|
-
2. **DETECT VIA GIT** —
|
|
54
|
-
3. **
|
|
55
|
-
4. **BIDIRECTIONAL** —
|
|
56
|
-
5. **
|
|
91
|
+
1. **AFTER `commit-manager`** — never before; the hash must be real.
|
|
92
|
+
2. **DETECT VIA GIT** — `git diff-tree -r HEAD`, never guess from session memory.
|
|
93
|
+
3. **EDIT KNOWN ANCHORS** — `documenter` uses `Edit` / `StrReplace` on existing domain files; never `Write` over them.
|
|
94
|
+
4. **BIDIRECTIONAL** — both ends of every connection updated atomically.
|
|
95
|
+
5. **CAP AT 8 KB / 20 commits / 10 attention / 5 P&S** — archive overflow into `<slug>.archive.md`.
|
|
96
|
+
6. **REGENERATE `_index.json` EVERY PASS** — derived; never hand-edited.
|
|
97
|
+
7. **SKIP META PATHS** — `.claude/**`, `docs/**`, `.github/**` do not get domain entries.
|
|
98
|
+
|
|
99
|
+
## See Also
|
|
100
|
+
|
|
101
|
+
- `documenter` v2.0.0 — applies the actions defined here
|
|
102
|
+
- `codebase-knowledge` v2.0.0 — reads what documenter wrote
|
|
103
|
+
- `domain-updater` v2.0.0 — appends session wisdom AFTER documenter
|
|
104
|
+
- `.claude/config/domain-mapping.json` — pattern → domain rules
|
|
@@ -1,40 +1,163 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: git-workflow
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "Git workflow conventions used by commit-manager v2.0.0+: Conventional Commits (no AI-attribution footers), branch naming, direct-to-main vs feature-branch flow, end-on-main rule, push policy, never force-push main, signed commits encouraged. Invoke when starting a feature, creating commits, pushing, or wiring git-related hooks."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Git Workflow
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
**ALWAYS invoke when starting work, creating commits, pushing, or wiring `pre-commit` / `Stop` hooks that touch git.**
|
|
10
|
+
|
|
11
|
+
> The git history is documentation. A reader six months from now should understand *why*, not just *what*. The commit-manager agent enforces the message format below; this skill defines what counts as compliant.
|
|
12
|
+
|
|
13
|
+
## 1. Branch naming
|
|
9
14
|
|
|
10
15
|
```
|
|
11
|
-
feature
|
|
12
|
-
fix
|
|
13
|
-
refactor
|
|
14
|
-
chore
|
|
16
|
+
feature/<short-kebab-description>
|
|
17
|
+
fix/<bug-id-or-description>
|
|
18
|
+
refactor/<area-changed>
|
|
19
|
+
chore/<maintenance-task>
|
|
20
|
+
docs/<scope>
|
|
21
|
+
ci/<workflow-area>
|
|
15
22
|
```
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
Rules:
|
|
25
|
+
- Always kebab-case, never spaces or underscores.
|
|
26
|
+
- ≤ 50 chars; longer goes in the PR description.
|
|
27
|
+
- One feature per branch.
|
|
28
|
+
|
|
29
|
+
## 2. Conventional Commits — required format
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
<type>(<scope>): <subject>
|
|
33
|
+
|
|
34
|
+
<body — wrapped at ~72 cols; bullets when ≥ 3 files affected>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Type | When |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `feat` | New user-visible capability |
|
|
40
|
+
| `fix` | Bug fix |
|
|
41
|
+
| `refactor` | Internal change, no behavior change |
|
|
42
|
+
| `perf` | Performance improvement |
|
|
43
|
+
| `test` | Tests added/changed only |
|
|
44
|
+
| `docs` | Documentation only |
|
|
45
|
+
| `chore` | Build, deps, tooling — nothing user-visible |
|
|
46
|
+
| `ci` | CI/CD config |
|
|
47
|
+
| `style` | Formatting only |
|
|
48
|
+
| `revert` | Reverts a prior commit |
|
|
49
|
+
| `build` | Build system / bundler config |
|
|
50
|
+
|
|
51
|
+
Subject:
|
|
52
|
+
- ≤ 50 chars
|
|
53
|
+
- imperative ("add login retry", not "added", not "adds")
|
|
54
|
+
- no trailing period
|
|
55
|
+
- lowercase first word (after the type/scope prefix)
|
|
56
|
+
|
|
57
|
+
Body (only when ≥ 3 files or non-obvious *why*):
|
|
58
|
+
- explain motivation, trade-offs, links to issues
|
|
59
|
+
- bullet list when summarizing multiple changes
|
|
60
|
+
|
|
61
|
+
### Example
|
|
18
62
|
|
|
19
63
|
```
|
|
20
|
-
|
|
64
|
+
feat(auth): add session refresh with rotating cookies
|
|
21
65
|
|
|
22
|
-
|
|
66
|
+
- Issue rotated session id on each login + every 7 days
|
|
67
|
+
- Persist previous id for 30s grace window to avoid races
|
|
68
|
+
- Drop legacy cookie name `sid` from response set; honor on read
|
|
69
|
+
```
|
|
23
70
|
|
|
71
|
+
### **No AI-attribution footers** *(commit-manager v2.0.0+ rule)*
|
|
72
|
+
|
|
73
|
+
Do **not** append:
|
|
74
|
+
```
|
|
24
75
|
Generated with Claude Code
|
|
25
76
|
Co-Authored-By: Claude <noreply@anthropic.com>
|
|
26
77
|
```
|
|
27
78
|
|
|
28
|
-
|
|
79
|
+
The commit message describes the change. Tooling provenance lives in CI artifacts, signed commits, or release metadata — not in every commit body.
|
|
80
|
+
|
|
81
|
+
## 3. Two valid flows
|
|
82
|
+
|
|
83
|
+
The repo's CLAUDE.md (or `git-workflow` overlay) declares which is in effect.
|
|
84
|
+
|
|
85
|
+
### Flow A — direct-to-main (small repos, single contributor, fast iteration)
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
1. Pull main, ensure clean tree
|
|
89
|
+
2. Make changes
|
|
90
|
+
3. Quality gate green
|
|
91
|
+
4. Commit → push to main
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Flow B — feature branch + merge (multi-contributor, CI gates required)
|
|
29
95
|
|
|
30
|
-
|
|
96
|
+
```
|
|
97
|
+
1. From clean main, create branch: git checkout -b feature/<name>
|
|
31
98
|
2. Work, commit incrementally
|
|
32
|
-
3.
|
|
33
|
-
4. Push
|
|
99
|
+
3. Quality gate green
|
|
100
|
+
4. Push branch: git push -u origin HEAD
|
|
101
|
+
5. Open PR; CI must pass
|
|
102
|
+
6. Merge (squash or rebase — never merge commit on main)
|
|
103
|
+
7. Delete branch local + remote
|
|
104
|
+
8. Checkout main, pull
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
End state in **both flows**: clean tree, on `main`, in sync with `origin/main`. The Stop validator enforces this.
|
|
108
|
+
|
|
109
|
+
## 4. Push policy
|
|
110
|
+
|
|
111
|
+
| Operation | Allowed | Notes |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `git push` to feature branch | ✅ Always | Force-push only if branch is yours and not yet reviewed |
|
|
114
|
+
| `git push` to main | ✅ When tree clean and CI green locally | Branch protection should reject otherwise |
|
|
115
|
+
| `git push --force-with-lease` to feature branch | ✅ Preferred over `--force` | Refuses if remote moved since your last fetch |
|
|
116
|
+
| `git push --force` to main | ❌ Never | Re-writes shared history |
|
|
117
|
+
| `git push --no-verify` | ❌ Never | Bypasses hooks intentionally configured to gate |
|
|
118
|
+
|
|
119
|
+
## 5. Pre-flight checklist (commit-manager runs this internally)
|
|
120
|
+
|
|
121
|
+
- [ ] Working tree status reviewed (`git status -sb`)
|
|
122
|
+
- [ ] Diff reviewed (`git diff --stat HEAD` then `git diff` if needed)
|
|
123
|
+
- [ ] Quality gate passed (typecheck / lint / tests / build)
|
|
124
|
+
- [ ] Security gate passed (`security-auditor` clean)
|
|
125
|
+
- [ ] No secrets in diff (gitleaks)
|
|
126
|
+
- [ ] CLAUDE.md `Last Change` updated when scope warrants it
|
|
127
|
+
- [ ] Conventional commit message drafted
|
|
128
|
+
- [ ] Push target confirmed (`origin main` vs `origin feature/*`)
|
|
129
|
+
|
|
130
|
+
## 6. Tags & releases
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Annotated tags only — lightweight tags are stripped by some hosts
|
|
134
|
+
git tag -a v1.2.3 -m "Release v1.2.3"
|
|
135
|
+
git push origin v1.2.3
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Semver: bump `MAJOR.MINOR.PATCH` per breaking/feature/fix.
|
|
139
|
+
|
|
140
|
+
## 7. Recovering from mistakes
|
|
141
|
+
|
|
142
|
+
| Situation | Fix |
|
|
143
|
+
|---|---|
|
|
144
|
+
| Bad commit not pushed | `git reset --soft HEAD~1`, fix, re-commit |
|
|
145
|
+
| Bad commit pushed to feature branch | `git commit --amend` then `git push --force-with-lease` |
|
|
146
|
+
| Bad commit pushed to main | `git revert <sha>` and push the revert (NEVER force-push main) |
|
|
147
|
+
| Pushed a secret | Revoke at provider FIRST, then `git filter-repo` + force-push (one-time, document it) |
|
|
148
|
+
| Wrong branch | `git stash`, `git checkout <correct>`, `git stash pop` |
|
|
34
149
|
|
|
35
150
|
## Rules
|
|
36
151
|
|
|
37
|
-
- NEVER force
|
|
38
|
-
-
|
|
39
|
-
- ALWAYS
|
|
40
|
-
-
|
|
152
|
+
- **NEVER** force-push `main` / `master`
|
|
153
|
+
- **NEVER** `--no-verify` to bypass hooks
|
|
154
|
+
- **ALWAYS** Conventional Commits, no AI-attribution footers
|
|
155
|
+
- **ALWAYS** end on `main` with a clean tree (Stop validator enforces)
|
|
156
|
+
- **ONE** feature per branch (or per direct-to-main commit batch)
|
|
157
|
+
|
|
158
|
+
## See Also
|
|
159
|
+
|
|
160
|
+
- `commit-manager` agent — drives the message + push pipeline
|
|
161
|
+
- `quality-gate` — the gate the commit-manager waits for
|
|
162
|
+
- `secrets-management` — gitleaks 3-layer (pre-commit, CI, push protection)
|
|
163
|
+
- `ci-pipelines` — branch-protection rules that mirror this skill
|
|
@@ -1,93 +1,271 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hook-development
|
|
3
|
-
version:
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
description: "Claude Code hook development for 2026. Covers all 9 supported events (UserPromptSubmit, PreToolUse, PostToolUse, Notification, Stop, SubagentStop, SessionStart, SessionEnd, PreCompact), stdin/stdout JSON contract, decision/continue/systemMessage outputs, matchers (tool patterns), exit-code semantics, cycle detection (stop_hook_active), settings.json registration, run-hook.sh dispatcher (bun → tsx → node fallback). Invoke when creating or modifying any .claude/hooks/* file or settings.json hook entry."
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# Hook Development — Claude Code Hooks
|
|
7
|
+
# Hook Development — Claude Code Hooks (2026)
|
|
7
8
|
|
|
8
9
|
**ALWAYS invoke when creating or modifying Claude Code hooks.**
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
> Hooks are user-controlled deterministic gates around the model. Anything you want **enforced**, not asked nicely — put in a hook. The agent cannot disable a hook from inside the conversation.
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|-------|------|----------|
|
|
14
|
-
| `UserPromptSubmit` | Before prompt is sent | Inject workflow, validate input |
|
|
15
|
-
| `Stop` | Before task completion | Validate state, block if dirty |
|
|
16
|
-
| `PreToolUse` | Before tool execution | Approve/block dangerous tools |
|
|
17
|
-
| `PostToolUse` | After tool execution | Log, validate output |
|
|
13
|
+
## 1. Supported events (Claude Code, 2026)
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
| Event | Fires | Common use |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| `SessionStart` | When a session begins (new chat or resume) | Inject onboarding context, restore state |
|
|
18
|
+
| `SessionEnd` | When a session ends cleanly | Flush logs, persist state, run cleanup |
|
|
19
|
+
| `UserPromptSubmit` | After user submits, before model receives | Inject workflow rules, block forbidden phrases |
|
|
20
|
+
| `PreToolUse` | Before any tool is invoked | Approve / block / rewrite tool calls (security gate) |
|
|
21
|
+
| `PostToolUse` | After tool completes | Log, validate output, react to specific tool results |
|
|
22
|
+
| `Notification` | When the agent is idle and notifies (e.g. waiting for input) | Desktop notifications, status forwarding |
|
|
23
|
+
| `Stop` | Before the agent ends its turn | Validate completion (clean tree, tests pass, docs updated) |
|
|
24
|
+
| `SubagentStop` | When a Task() subagent finishes | Validate subagent output, persist artifacts |
|
|
25
|
+
| `PreCompact` | Before transcript compaction is triggered | Snapshot/export full transcript first |
|
|
26
|
+
|
|
27
|
+
`Stop` and `Subagent` events are the most commonly customized. `PreToolUse` is the security workhorse.
|
|
28
|
+
|
|
29
|
+
## 2. File layout
|
|
20
30
|
|
|
21
31
|
```
|
|
22
|
-
.claude/
|
|
23
|
-
├──
|
|
24
|
-
├──
|
|
25
|
-
|
|
32
|
+
.claude/
|
|
33
|
+
├── hooks/
|
|
34
|
+
│ ├── run-hook.sh # Entry point: bun → tsx → node fallback
|
|
35
|
+
│ ├── session-start.ts
|
|
36
|
+
│ ├── session-end.ts
|
|
37
|
+
│ ├── user-prompt-submit.ts
|
|
38
|
+
│ ├── pre-tool-use.ts
|
|
39
|
+
│ ├── post-tool-use.ts
|
|
40
|
+
│ ├── stop-validator.ts
|
|
41
|
+
│ ├── subagent-stop.ts
|
|
42
|
+
│ ├── pre-compact.ts
|
|
43
|
+
│ └── lib/ # shared helpers (cmd(), readStdin(), etc.)
|
|
44
|
+
└── settings.json # registers hooks
|
|
26
45
|
```
|
|
27
46
|
|
|
28
|
-
##
|
|
47
|
+
## 3. Stdin contract (per event)
|
|
48
|
+
|
|
49
|
+
All hook input arrives as **a single JSON object on stdin**. Read with a timeout — never hang.
|
|
29
50
|
|
|
30
51
|
```typescript
|
|
31
|
-
//
|
|
32
|
-
interface
|
|
33
|
-
user_prompt: string;
|
|
52
|
+
// Common envelope (most events)
|
|
53
|
+
interface HookInput {
|
|
34
54
|
session_id: string;
|
|
55
|
+
cwd: string;
|
|
56
|
+
transcript_path?: string;
|
|
57
|
+
hook_event_name: string; // e.g. "Stop"
|
|
35
58
|
}
|
|
36
59
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
stop_hook_active?: boolean; // Cycle detection
|
|
40
|
-
transcript?: string;
|
|
60
|
+
interface UserPromptSubmitInput extends HookInput {
|
|
61
|
+
user_prompt: string;
|
|
41
62
|
}
|
|
42
|
-
```
|
|
43
63
|
|
|
44
|
-
|
|
64
|
+
interface PreToolUseInput extends HookInput {
|
|
65
|
+
tool_name: string; // "Bash" | "Edit" | "Write" | ...
|
|
66
|
+
tool_input: Record<string, unknown>;
|
|
67
|
+
}
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
interface PostToolUseInput extends PreToolUseInput {
|
|
70
|
+
tool_response: unknown;
|
|
71
|
+
tool_error?: string;
|
|
72
|
+
}
|
|
49
73
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
74
|
+
interface StopInput extends HookInput {
|
|
75
|
+
stop_hook_active?: boolean; // TRUE if a previous Stop hook already blocked — don't loop
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface SubagentStopInput extends StopInput {
|
|
79
|
+
subagent_id: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface PreCompactInput extends HookInput {
|
|
83
|
+
trigger: "manual" | "auto";
|
|
84
|
+
}
|
|
53
85
|
```
|
|
54
86
|
|
|
55
|
-
##
|
|
87
|
+
## 4. Stdout contract (per event)
|
|
56
88
|
|
|
57
|
-
|
|
58
|
-
#!/usr/bin/env node
|
|
59
|
-
import { execSync } from 'child_process';
|
|
89
|
+
Output a **single JSON object on stdout**. Anything else (or invalid JSON) is treated as advisory only.
|
|
60
90
|
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
```typescript
|
|
92
|
+
// Universal fields
|
|
93
|
+
interface HookOutput {
|
|
94
|
+
continue?: boolean; // false = stop the agent now (Stop family)
|
|
95
|
+
decision?: "approve" | "block"; // gate verdict
|
|
96
|
+
reason?: string; // shown to user / logged
|
|
97
|
+
systemMessage?: string; // injected as system message (UserPromptSubmit, SessionStart)
|
|
98
|
+
hookSpecificOutput?: Record<string, unknown>;
|
|
63
99
|
}
|
|
100
|
+
```
|
|
64
101
|
|
|
65
|
-
|
|
66
|
-
|
|
102
|
+
PreToolUse — block / approve / rewrite a tool call:
|
|
103
|
+
```json
|
|
104
|
+
{ "decision": "approve", "reason": "Edit on src/ allowed" }
|
|
105
|
+
{ "decision": "block", "reason": "Write outside src/ rejected" }
|
|
106
|
+
```
|
|
67
107
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
108
|
+
Stop — block prevents finalization until issues are fixed:
|
|
109
|
+
```json
|
|
110
|
+
{ "continue": true, "decision": "block", "reason": "Uncommitted files" }
|
|
111
|
+
```
|
|
71
112
|
|
|
72
|
-
|
|
113
|
+
SessionStart — inject context:
|
|
114
|
+
```json
|
|
115
|
+
{ "systemMessage": "ROUTINE: research → plan → implement → test → commit" }
|
|
73
116
|
```
|
|
74
117
|
|
|
75
|
-
##
|
|
118
|
+
## 5. Exit-code semantics
|
|
119
|
+
|
|
120
|
+
| Exit | Meaning |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `0` | Success — JSON output applied if present |
|
|
123
|
+
| `2` | **Blocking** — message printed to stderr is shown to the user (alternative to `decision: "block"`) |
|
|
124
|
+
| anything else | Non-blocking error — logged, agent continues |
|
|
125
|
+
|
|
126
|
+
**Never** exit non-zero on a transient error (e.g. network) unless you intend to block. Hooks must be deterministic.
|
|
127
|
+
|
|
128
|
+
## 6. `settings.json` registration
|
|
76
129
|
|
|
77
130
|
```json
|
|
78
131
|
{
|
|
79
132
|
"hooks": {
|
|
80
|
-
"
|
|
81
|
-
|
|
133
|
+
"SessionStart": [
|
|
134
|
+
{
|
|
135
|
+
"hooks": [{
|
|
136
|
+
"type": "command",
|
|
137
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/run-hook.sh\" session-start",
|
|
138
|
+
"timeout": 10
|
|
139
|
+
}]
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"UserPromptSubmit": [
|
|
143
|
+
{
|
|
144
|
+
"matcher": "",
|
|
145
|
+
"hooks": [{
|
|
146
|
+
"type": "command",
|
|
147
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/run-hook.sh\" user-prompt-submit",
|
|
148
|
+
"timeout": 10
|
|
149
|
+
}]
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
"PreToolUse": [
|
|
153
|
+
{
|
|
154
|
+
"matcher": "Bash|Write|Edit",
|
|
155
|
+
"hooks": [{
|
|
156
|
+
"type": "command",
|
|
157
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/run-hook.sh\" pre-tool-use",
|
|
158
|
+
"timeout": 10
|
|
159
|
+
}]
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
"Stop": [
|
|
163
|
+
{
|
|
164
|
+
"hooks": [{
|
|
165
|
+
"type": "command",
|
|
166
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/run-hook.sh\" stop-validator",
|
|
167
|
+
"timeout": 30
|
|
168
|
+
}]
|
|
169
|
+
}
|
|
170
|
+
]
|
|
82
171
|
}
|
|
83
172
|
}
|
|
84
173
|
```
|
|
85
174
|
|
|
86
|
-
|
|
175
|
+
`matcher` is a regex against `tool_name` (PreToolUse / PostToolUse) or empty for "all". Multiple entries per event are run in registration order.
|
|
176
|
+
|
|
177
|
+
## 7. `run-hook.sh` dispatcher
|
|
178
|
+
|
|
179
|
+
Survives whichever runtime the user has installed. Always-zero exit so hook errors don't kill the session.
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
#!/usr/bin/env bash
|
|
183
|
+
# .claude/hooks/run-hook.sh
|
|
184
|
+
set -uo pipefail
|
|
185
|
+
HOOK_NAME="${1:?missing hook name}"
|
|
186
|
+
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
187
|
+
HOOK_FILE="$HOOK_DIR/${HOOK_NAME}.ts"
|
|
188
|
+
|
|
189
|
+
[[ -f "$HOOK_FILE" ]] || { echo '{"continue":true}'; exit 0; }
|
|
190
|
+
|
|
191
|
+
if command -v bun >/dev/null 2>&1; then bun "$HOOK_FILE"
|
|
192
|
+
elif command -v tsx >/dev/null 2>&1; then tsx "$HOOK_FILE"
|
|
193
|
+
elif command -v node >/dev/null 2>&1; then npx --yes tsx "$HOOK_FILE"
|
|
194
|
+
else echo '{"continue":true}'; exit 0
|
|
195
|
+
fi
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 8. Stop validator template (gate-aware, with cycle detection)
|
|
199
|
+
|
|
200
|
+
Use `execFileSync` (not `execSync` with shell strings) — passes args as an array, immune to shell injection.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
#!/usr/bin/env node
|
|
204
|
+
import { execFileSync } from 'node:child_process';
|
|
205
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
206
|
+
|
|
207
|
+
function git(...args: string[]): string {
|
|
208
|
+
try { return execFileSync('git', args, { encoding: 'utf8' }).trim(); } catch { return ''; }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const input = JSON.parse(readFileSync(0, 'utf8')); // stdin
|
|
212
|
+
|
|
213
|
+
// Cycle detection — if we already blocked once, approve to avoid infinite loop
|
|
214
|
+
if (input.stop_hook_active) {
|
|
215
|
+
console.log(JSON.stringify({ continue: false, decision: 'approve' }));
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const dirty = git('status', '--porcelain');
|
|
220
|
+
const issues: string[] = [];
|
|
221
|
+
|
|
222
|
+
if (dirty) issues.push(`GIT_TREE_NOT_CLEAN: ${dirty.split('\n').length} file(s)`);
|
|
223
|
+
if (existsSync('CLAUDE.md') &&
|
|
224
|
+
!readFileSync('CLAUDE.md', 'utf8').includes('Last Change')) issues.push('CLAUDE_MD_NOT_UPDATED');
|
|
225
|
+
|
|
226
|
+
const result = issues.length === 0
|
|
227
|
+
? { continue: false, decision: 'approve', reason: 'All checks passed' }
|
|
228
|
+
: { continue: true, decision: 'block', reason: issues.join(' | ') };
|
|
229
|
+
|
|
230
|
+
console.log(JSON.stringify(result));
|
|
231
|
+
process.exit(0);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 9. Performance & safety rules
|
|
235
|
+
|
|
236
|
+
1. **Timeouts:** 10s for `UserPromptSubmit` / `PreToolUse` / `SessionStart`; 30s for `Stop` / `SubagentStop`.
|
|
237
|
+
2. **Always exit 0** unless intentionally blocking with exit 2 — non-zero kills the session.
|
|
238
|
+
3. **Read stdin with timeout** — `readFileSync(0, 'utf8')` is fine (it returns immediately when EOF).
|
|
239
|
+
4. **Output valid JSON** — invalid JSON is treated as advisory and silently dropped.
|
|
240
|
+
5. **Cycle detection** — check `stop_hook_active` in `Stop` / `SubagentStop`. Without this, a buggy hook can lock the agent in an infinite block-fix-block loop.
|
|
241
|
+
6. **No network calls** in hot-path hooks (`PreToolUse`, `UserPromptSubmit`) — they fire on every tool call / prompt.
|
|
242
|
+
7. **Idempotent** — hooks may be invoked twice (retries, restarts). Don't write to immutable state without a guard.
|
|
243
|
+
8. **Use `execFileSync`, never `execSync` with a shell string** — the latter is shell-injection-prone if any input ever flows in.
|
|
244
|
+
|
|
245
|
+
## 10. Common patterns
|
|
246
|
+
|
|
247
|
+
| Goal | Event | Pattern |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| "Inject the current routine on every prompt" | `UserPromptSubmit` | `{ continue: true, systemMessage: "ROUTINE: ..." }` |
|
|
250
|
+
| "Block writes outside `src/`" | `PreToolUse` | match `Write\|Edit`, inspect `tool_input.path`, `decision: "block"` if outside |
|
|
251
|
+
| "Run quality gate before finishing" | `Stop` | run typecheck/lint/tests, `decision: "block"` if any fail |
|
|
252
|
+
| "Persist subagent output to disk" | `SubagentStop` | parse `tool_response`, write to `.claude/subagent-outputs/{id}.json` |
|
|
253
|
+
| "Snapshot transcript before compaction" | `PreCompact` | copy `transcript_path` to `.claude/transcripts/{session_id}.jsonl` |
|
|
254
|
+
|
|
255
|
+
## FORBIDDEN
|
|
256
|
+
|
|
257
|
+
| Pattern | Why |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `process.exit(1)` on transient error | Kills the session |
|
|
260
|
+
| Hook output not valid JSON | Dropped silently — no enforcement |
|
|
261
|
+
| Network call in `PreToolUse` | Adds latency to every tool call |
|
|
262
|
+
| Forgetting `stop_hook_active` check | Infinite block loop |
|
|
263
|
+
| Writing to `CLAUDE.md` from `Stop` hook | Triggers another `Stop` cycle |
|
|
264
|
+
| Hardcoded user paths (`~/Users/me/...`) | Won't run on other machines |
|
|
265
|
+
| Shell-string `execSync` for git/fs commands | Shell-injection surface |
|
|
266
|
+
|
|
267
|
+
## See Also
|
|
87
268
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
4. **Output valid JSON to stdout** — Claude Code parses it
|
|
92
|
-
5. **Cycle detection** — check `stop_hook_active` flag in Stop hooks
|
|
93
|
-
6. **Keep hooks fast** — timeout applies (10s for prompt, 30s for stop)
|
|
269
|
+
- `quality-gate` — the Stop validator's typical payload
|
|
270
|
+
- `git-workflow` — branch / dirty-tree checks
|
|
271
|
+
- `commit-manager` — pairs with Stop validator (validator detects, commit-manager fixes)
|