claude-code-hookkit 1.0.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/LICENSE +21 -0
- package/README.md +311 -0
- package/dist/add-O4OSFQ76.js +140 -0
- package/dist/chunk-2BZZUQQ3.js +34 -0
- package/dist/chunk-LRXKKJDU.js +101 -0
- package/dist/chunk-PEDGREZY.js +46 -0
- package/dist/chunk-QKT647BI.js +30 -0
- package/dist/chunk-XLX5K6TZ.js +113 -0
- package/dist/cli.js +76 -0
- package/dist/create-DBLA6PTS.js +268 -0
- package/dist/doctor-UBK2C2TW.js +137 -0
- package/dist/info-FLYMAHDX.js +84 -0
- package/dist/init-RHEFGGUF.js +70 -0
- package/dist/list-SCSGYOBR.js +54 -0
- package/dist/remove-Z5QIW45P.js +109 -0
- package/dist/restore-7JQ3CHWZ.js +31 -0
- package/dist/test-ZRRLZ62R.js +194 -0
- package/package.json +59 -0
- package/registry/hooks/cost-tracker.sh +44 -0
- package/registry/hooks/error-advisor.sh +114 -0
- package/registry/hooks/exit-code-enforcer.sh +76 -0
- package/registry/hooks/fixtures/cost-tracker/allow-bash-tool.json +5 -0
- package/registry/hooks/fixtures/cost-tracker/allow-no-session.json +5 -0
- package/registry/hooks/fixtures/error-advisor/allow-enoent.json +5 -0
- package/registry/hooks/fixtures/error-advisor/allow-no-error.json +5 -0
- package/registry/hooks/fixtures/exit-code-enforcer/allow-npm-test.json +5 -0
- package/registry/hooks/fixtures/exit-code-enforcer/block-rm-rf.json +5 -0
- package/registry/hooks/fixtures/post-edit-lint/allow-ts-file.json +5 -0
- package/registry/hooks/fixtures/post-edit-lint/allow-unknown-ext.json +5 -0
- package/registry/hooks/fixtures/sensitive-path-guard/allow-src.json +5 -0
- package/registry/hooks/fixtures/sensitive-path-guard/block-env.json +5 -0
- package/registry/hooks/fixtures/ts-check/allow-non-ts.json +5 -0
- package/registry/hooks/fixtures/ts-check/allow-ts-file.json +5 -0
- package/registry/hooks/fixtures/web-budget-gate/allow-within-budget.json +6 -0
- package/registry/hooks/fixtures/web-budget-gate/block-over-budget.json +6 -0
- package/registry/hooks/post-edit-lint.sh +82 -0
- package/registry/hooks/sensitive-path-guard.sh +103 -0
- package/registry/hooks/ts-check.sh +98 -0
- package/registry/hooks/web-budget-gate.sh +60 -0
- package/registry/registry.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Austin Amelone
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# claude-code-hookkit
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/claude-code-hookkit)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
**husky for Claude Code** — install, manage, test, and share hooks with a single command.
|
|
7
|
+
|
|
8
|
+
Go from zero to production-grade Claude Code hooks in under 60 seconds:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx claude-code-hookkit init && npx claude-code-hookkit add security-pack
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## What Are Claude Code Hooks?
|
|
17
|
+
|
|
18
|
+
Claude Code hooks are shell commands triggered by Claude Code events (PreToolUse, PostToolUse, SessionStart, Stop). They run automatically and can:
|
|
19
|
+
|
|
20
|
+
- **Block** dangerous operations before they execute (exit code 2)
|
|
21
|
+
- **Observe** and log what Claude does (exit code 0, advisory)
|
|
22
|
+
- **Provide feedback** after actions complete
|
|
23
|
+
|
|
24
|
+
Hooks are configured in `~/.claude/settings.json` under the `hooks` key. `claude-code-hookkit` manages that configuration for you — non-destructively, with backups.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Initialize hook directory and seed settings.json
|
|
32
|
+
npx claude-code-hookkit init
|
|
33
|
+
|
|
34
|
+
# Install the security pack (sensitive-path-guard + exit-code-enforcer)
|
|
35
|
+
npx claude-code-hookkit add security-pack
|
|
36
|
+
|
|
37
|
+
# See what's installed
|
|
38
|
+
npx claude-code-hookkit list
|
|
39
|
+
|
|
40
|
+
# Verify everything is healthy
|
|
41
|
+
npx claude-code-hookkit doctor
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Command Reference
|
|
47
|
+
|
|
48
|
+
### `claude-code-hookkit init`
|
|
49
|
+
|
|
50
|
+
Scaffold the hook directory and seed `settings.json`.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
claude-code-hookkit init # project scope (default)
|
|
54
|
+
claude-code-hookkit init --scope user # user-level settings (~/.claude/settings.json)
|
|
55
|
+
claude-code-hookkit init --dry-run # preview without writing
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### `claude-code-hookkit add <name>`
|
|
59
|
+
|
|
60
|
+
Install a hook or pack from the bundled registry.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
claude-code-hookkit add sensitive-path-guard # single hook
|
|
64
|
+
claude-code-hookkit add security-pack # install entire pack
|
|
65
|
+
claude-code-hookkit add cost-tracker --scope user # user-level install
|
|
66
|
+
claude-code-hookkit add post-edit-lint --dry-run # preview changes
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `claude-code-hookkit remove <name>`
|
|
70
|
+
|
|
71
|
+
Remove an installed hook (script + settings.json entry).
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
claude-code-hookkit remove sensitive-path-guard
|
|
75
|
+
claude-code-hookkit remove post-edit-lint --scope user
|
|
76
|
+
claude-code-hookkit remove web-budget-gate --dry-run
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `claude-code-hookkit list`
|
|
80
|
+
|
|
81
|
+
List all available hooks with installed status, event type, and pack.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
claude-code-hookkit list
|
|
85
|
+
claude-code-hookkit list --scope user
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `claude-code-hookkit test <hook>`
|
|
89
|
+
|
|
90
|
+
Test a hook with its bundled fixture data. Validates exit code and output.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
claude-code-hookkit test sensitive-path-guard # test single hook
|
|
94
|
+
claude-code-hookkit test --all # test all installed hooks
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `claude-code-hookkit create <name>`
|
|
98
|
+
|
|
99
|
+
Scaffold a custom hook from a template.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
claude-code-hookkit create my-guard --event PreToolUse --matcher Bash
|
|
103
|
+
claude-code-hookkit create session-logger --event SessionStart
|
|
104
|
+
claude-code-hookkit create cleanup --event Stop
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Generates a working shell script with proper shebang, stdin JSON parsing, and a test fixture skeleton.
|
|
108
|
+
|
|
109
|
+
### `claude-code-hookkit doctor`
|
|
110
|
+
|
|
111
|
+
Validate installation health: script existence, permissions, settings.json validity, conflicting hooks.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
claude-code-hookkit doctor
|
|
115
|
+
claude-code-hookkit doctor --scope user
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `claude-code-hookkit restore`
|
|
119
|
+
|
|
120
|
+
Revert settings.json to the last backup (created automatically before every write).
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
claude-code-hookkit restore
|
|
124
|
+
claude-code-hookkit restore --scope user
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `claude-code-hookkit info <hook>`
|
|
128
|
+
|
|
129
|
+
Show full details for a hook: description, event, matcher, pack, and example input JSON.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
claude-code-hookkit info sensitive-path-guard
|
|
133
|
+
claude-code-hookkit info web-budget-gate
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Hook Registry
|
|
139
|
+
|
|
140
|
+
All 7 bundled hooks ship with the package — no network required.
|
|
141
|
+
|
|
142
|
+
| Hook | Description | Event | Matcher | Pack |
|
|
143
|
+
|------|-------------|-------|---------|------|
|
|
144
|
+
| `sensitive-path-guard` | Blocks writes to .env, credentials, private keys | PreToolUse | Edit\|Write | security-pack |
|
|
145
|
+
| `exit-code-enforcer` | Blocks known-dangerous shell commands (rm -rf /, fork bombs, etc.) | PreToolUse | Bash | security-pack |
|
|
146
|
+
| `post-edit-lint` | Runs linter on files after Claude edits them | PostToolUse | Write\|Edit | quality-pack |
|
|
147
|
+
| `ts-check` | Runs TypeScript type checking after code changes | PostToolUse | Write\|Edit | quality-pack |
|
|
148
|
+
| `web-budget-gate` | Limits web search/fetch calls per session to control costs | PreToolUse | WebSearch\|WebFetch | cost-pack |
|
|
149
|
+
| `cost-tracker` | Tracks tool usage costs per session | PostToolUse | — | cost-pack |
|
|
150
|
+
| `error-advisor` | Provides contextual fix suggestions when commands fail | PostToolUse | Bash | error-pack |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Packs
|
|
155
|
+
|
|
156
|
+
Install related hooks together in one command.
|
|
157
|
+
|
|
158
|
+
### `security-pack`
|
|
159
|
+
|
|
160
|
+
Essential security hooks. Blocks writes to sensitive files and dangerous shell commands.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
claude-code-hookkit add security-pack
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Includes: `sensitive-path-guard`, `exit-code-enforcer`
|
|
167
|
+
|
|
168
|
+
### `quality-pack`
|
|
169
|
+
|
|
170
|
+
Code quality hooks that run automatically after Claude edits files.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
claude-code-hookkit add quality-pack
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Includes: `post-edit-lint`, `ts-check`
|
|
177
|
+
|
|
178
|
+
### `cost-pack`
|
|
179
|
+
|
|
180
|
+
Cost control hooks. Limit web calls per session and track tool usage.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
claude-code-hookkit add cost-pack
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Includes: `web-budget-gate`, `cost-tracker`
|
|
187
|
+
|
|
188
|
+
### `error-pack`
|
|
189
|
+
|
|
190
|
+
Error recovery. When a Bash command fails, this hook analyzes the output and suggests contextual fixes.
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
claude-code-hookkit add error-pack
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Includes: `error-advisor`
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## How Hooks Work
|
|
201
|
+
|
|
202
|
+
Claude Code evaluates hooks from your `settings.json`. Example entry added by `claude-code-hookkit add`:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"hooks": {
|
|
207
|
+
"PreToolUse": [
|
|
208
|
+
{
|
|
209
|
+
"matcher": "Edit|Write",
|
|
210
|
+
"hooks": [
|
|
211
|
+
{
|
|
212
|
+
"type": "command",
|
|
213
|
+
"command": "/path/to/.claude/hooks/sensitive-path-guard.sh"
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Exit codes:**
|
|
223
|
+
- `0` — allow the operation (or advisory-only PostToolUse hook)
|
|
224
|
+
- `2` — block the operation (Claude Code spec; exit 1 does not block)
|
|
225
|
+
|
|
226
|
+
**Input format:** Claude Code passes JSON via stdin. Hooks read it with `INPUT=$(cat)` and parse fields with `grep`/`sed` (no `jq` required — POSIX-compatible).
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Creating Custom Hooks
|
|
231
|
+
|
|
232
|
+
Scaffold a hook with the right structure:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
claude-code-hookkit create my-guard --event PreToolUse --matcher Bash
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
This creates:
|
|
239
|
+
- `.claude/hooks/my-guard.sh` — working hook script
|
|
240
|
+
- `.claude/hooks/fixtures/my-guard/allow-example.json` — test fixture skeleton
|
|
241
|
+
|
|
242
|
+
The generated script handles stdin JSON parsing, includes commented examples, and uses the correct exit codes. Edit the pattern-matching logic and add your fixture test cases, then run:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
claude-code-hookkit test my-guard
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Hook Template Pattern
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
#!/bin/bash
|
|
252
|
+
INPUT=$(cat)
|
|
253
|
+
|
|
254
|
+
# Extract what you need from tool JSON
|
|
255
|
+
VALUE=$(printf '%s' "$INPUT" | grep -o '"field"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: *"//; s/"//')
|
|
256
|
+
|
|
257
|
+
# Your logic here
|
|
258
|
+
if [[ "$VALUE" == "bad-value" ]]; then
|
|
259
|
+
printf 'BLOCKED: reason\n' >&2
|
|
260
|
+
exit 2
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
exit 0
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Fixture Format
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{
|
|
270
|
+
"description": "What this fixture tests",
|
|
271
|
+
"input": { "tool_input": { "file_path": ".env" } },
|
|
272
|
+
"expectedExitCode": 2
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Settings Scope
|
|
279
|
+
|
|
280
|
+
All commands support `--scope`:
|
|
281
|
+
|
|
282
|
+
| Scope | Settings File | Hook Directory |
|
|
283
|
+
|-------|---------------|----------------|
|
|
284
|
+
| `project` (default) | `.claude/settings.json` | `.claude/hooks/` |
|
|
285
|
+
| `user` | `~/.claude/settings.json` | `~/.claude/hooks/` |
|
|
286
|
+
| `local` | `.claude/settings.local.json` | `.claude/hooks/` |
|
|
287
|
+
|
|
288
|
+
`add` and `init` always perform a deep merge — your existing settings are never overwritten.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Contributing
|
|
293
|
+
|
|
294
|
+
1. Fork the repo
|
|
295
|
+
2. Add your hook to `registry/hooks/` with inline comments
|
|
296
|
+
3. Add metadata to `registry/registry.json`
|
|
297
|
+
4. Add test fixtures to `registry/hooks/fixtures/<hook-name>/`
|
|
298
|
+
5. Run `npm test` — all fixtures must pass
|
|
299
|
+
6. Submit a PR with a description of what the hook does and why it's useful
|
|
300
|
+
|
|
301
|
+
Hooks must be:
|
|
302
|
+
- POSIX-compatible (macOS bash 3.2 + Linux bash 5.x)
|
|
303
|
+
- No external dependencies (no `jq`, no `python`, no `node`)
|
|
304
|
+
- Exit code 2 to block, exit code 0 to allow
|
|
305
|
+
- Include at least one allow fixture and one block fixture
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT — Copyright (c) 2026 Austin Amelone
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
log
|
|
4
|
+
} from "./chunk-2BZZUQQ3.js";
|
|
5
|
+
import {
|
|
6
|
+
applyMerge
|
|
7
|
+
} from "./chunk-LRXKKJDU.js";
|
|
8
|
+
import "./chunk-QKT647BI.js";
|
|
9
|
+
import {
|
|
10
|
+
getHook,
|
|
11
|
+
getPack
|
|
12
|
+
} from "./chunk-XLX5K6TZ.js";
|
|
13
|
+
import {
|
|
14
|
+
getHooksDir,
|
|
15
|
+
getSettingsPath
|
|
16
|
+
} from "./chunk-PEDGREZY.js";
|
|
17
|
+
|
|
18
|
+
// src/commands/add.ts
|
|
19
|
+
import { copyFile, chmod, mkdir } from "fs/promises";
|
|
20
|
+
import { existsSync } from "fs";
|
|
21
|
+
import { join, resolve } from "path";
|
|
22
|
+
import { fileURLToPath } from "url";
|
|
23
|
+
import { dirname } from "path";
|
|
24
|
+
function getDefaultSourceHooksDir() {
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
let dir = dirname(__filename);
|
|
27
|
+
for (let i = 0; i < 5; i++) {
|
|
28
|
+
const candidate = join(dir, "registry", "hooks");
|
|
29
|
+
if (existsSync(candidate)) return candidate;
|
|
30
|
+
const parent = resolve(dir, "..");
|
|
31
|
+
if (parent === dir) break;
|
|
32
|
+
dir = parent;
|
|
33
|
+
}
|
|
34
|
+
return join(dirname(__filename), "..", "..", "registry", "hooks");
|
|
35
|
+
}
|
|
36
|
+
async function _addAt(opts) {
|
|
37
|
+
const { settingsPath, hooksDir, hookName, dryRun = false } = opts;
|
|
38
|
+
const sourceHooksDir = opts.sourceHooksDir ?? getDefaultSourceHooksDir();
|
|
39
|
+
const hook = getHook(hookName);
|
|
40
|
+
if (!hook) {
|
|
41
|
+
log.error(`Unknown hook: "${hookName}". Run "claude-code-hookkit list" to see available hooks.`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const srcPath = join(sourceHooksDir, hook.scriptFile);
|
|
45
|
+
const destPath = join(hooksDir, hook.scriptFile);
|
|
46
|
+
if (dryRun) {
|
|
47
|
+
log.dryRun(`Would copy: ${srcPath} -> ${destPath}`);
|
|
48
|
+
log.dryRun(`Would chmod +x: ${destPath}`);
|
|
49
|
+
log.dryRun(`Would add ${hook.event}${hook.matcher ? ` [${hook.matcher}]` : ""} hook to settings.json`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await mkdir(hooksDir, { recursive: true });
|
|
53
|
+
const result = await applyMerge({
|
|
54
|
+
settingsPath,
|
|
55
|
+
newHooks: [
|
|
56
|
+
{
|
|
57
|
+
event: hook.event,
|
|
58
|
+
matcher: hook.matcher,
|
|
59
|
+
hook: { type: "command", command: destPath }
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
dryRun: false
|
|
63
|
+
});
|
|
64
|
+
if (result.added.length > 0) {
|
|
65
|
+
await copyFile(srcPath, destPath);
|
|
66
|
+
await chmod(destPath, 493);
|
|
67
|
+
}
|
|
68
|
+
if (result.added.length > 0) {
|
|
69
|
+
log.success(`Installed hook: ${hook.name}`);
|
|
70
|
+
log.dim(` Script: ${destPath}`);
|
|
71
|
+
if (hook.pack) {
|
|
72
|
+
log.dim(` Pack: ${hook.pack}`);
|
|
73
|
+
}
|
|
74
|
+
log.dim(` Event: ${hook.event}${hook.matcher ? ` [matcher: ${hook.matcher}]` : ""}`);
|
|
75
|
+
} else if (result.skipped.length > 0) {
|
|
76
|
+
log.warn(`Hook "${hook.name}" is already installed (skipped).`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function _addPackAt(opts) {
|
|
80
|
+
const { settingsPath, hooksDir, packName, dryRun = false } = opts;
|
|
81
|
+
const sourceHooksDir = opts.sourceHooksDir ?? getDefaultSourceHooksDir();
|
|
82
|
+
const pack = getPack(packName);
|
|
83
|
+
if (!pack) {
|
|
84
|
+
log.error(`Unknown hook or pack: "${packName}". Run "claude-code-hookkit list" to see available options.`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
log.info(`Installing ${packName} (${pack.hooks.length} hooks)...`);
|
|
88
|
+
const installed = [];
|
|
89
|
+
const skipped = [];
|
|
90
|
+
for (const hookName of pack.hooks) {
|
|
91
|
+
const hook = getHook(hookName);
|
|
92
|
+
if (!hook) {
|
|
93
|
+
log.warn(`Pack "${packName}" references unknown hook "${hookName}" \u2014 skipping.`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (dryRun) {
|
|
97
|
+
const srcPath = join(sourceHooksDir, hook.scriptFile);
|
|
98
|
+
const destPath = join(hooksDir, hook.scriptFile);
|
|
99
|
+
log.dryRun(`Would install hook: ${hookName}`);
|
|
100
|
+
log.dryRun(` ${srcPath} -> ${destPath}`);
|
|
101
|
+
installed.push(hookName);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
await _addAt({ settingsPath, hooksDir, sourceHooksDir, hookName, dryRun });
|
|
105
|
+
installed.push(hookName);
|
|
106
|
+
}
|
|
107
|
+
if (!dryRun) {
|
|
108
|
+
log.success(`Installed ${packName} (${installed.length} hooks): ${installed.join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function addCommand(opts) {
|
|
112
|
+
const validScopes = ["user", "project", "local"];
|
|
113
|
+
const scope = validScopes.includes(opts.scope) ? opts.scope : "project";
|
|
114
|
+
const settingsPath = getSettingsPath(scope);
|
|
115
|
+
const hooksDir = getHooksDir(scope);
|
|
116
|
+
if (getPack(opts.hookName)) {
|
|
117
|
+
await _addPackAt({
|
|
118
|
+
settingsPath,
|
|
119
|
+
hooksDir,
|
|
120
|
+
packName: opts.hookName,
|
|
121
|
+
dryRun: opts.dryRun
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (getHook(opts.hookName)) {
|
|
126
|
+
await _addAt({
|
|
127
|
+
settingsPath,
|
|
128
|
+
hooksDir,
|
|
129
|
+
hookName: opts.hookName,
|
|
130
|
+
dryRun: opts.dryRun
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
log.error(`Unknown hook or pack: "${opts.hookName}". Run "claude-code-hookkit list" to see available options.`);
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
_addAt,
|
|
138
|
+
_addPackAt,
|
|
139
|
+
addCommand
|
|
140
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/logger.ts
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
var log = {
|
|
6
|
+
/** Standard informational message */
|
|
7
|
+
info(msg) {
|
|
8
|
+
console.log(msg);
|
|
9
|
+
},
|
|
10
|
+
/** Success message in green */
|
|
11
|
+
success(msg) {
|
|
12
|
+
console.log(pc.green(msg));
|
|
13
|
+
},
|
|
14
|
+
/** Warning message in yellow */
|
|
15
|
+
warn(msg) {
|
|
16
|
+
console.warn(pc.yellow(msg));
|
|
17
|
+
},
|
|
18
|
+
/** Error message in red (to stderr) */
|
|
19
|
+
error(msg) {
|
|
20
|
+
console.error(pc.red(msg));
|
|
21
|
+
},
|
|
22
|
+
/** Dimmed/secondary text */
|
|
23
|
+
dim(msg) {
|
|
24
|
+
console.log(pc.dim(msg));
|
|
25
|
+
},
|
|
26
|
+
/** Dry-run prefixed message in yellow */
|
|
27
|
+
dryRun(msg) {
|
|
28
|
+
console.log(pc.yellow("[DRY RUN]") + " " + msg);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
log
|
|
34
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createBackup
|
|
4
|
+
} from "./chunk-QKT647BI.js";
|
|
5
|
+
|
|
6
|
+
// src/config/manager.ts
|
|
7
|
+
import { readFile, writeFile } from "fs/promises";
|
|
8
|
+
|
|
9
|
+
// src/config/merger.ts
|
|
10
|
+
function isDuplicate(groups, matcher, command) {
|
|
11
|
+
return groups.some((group) => {
|
|
12
|
+
const matcherMatches = group.matcher === matcher;
|
|
13
|
+
const commandMatches = group.hooks.some((h) => h.command === command);
|
|
14
|
+
return matcherMatches && commandMatches;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function mergeHooks(input) {
|
|
18
|
+
const settings = structuredClone(input.existing);
|
|
19
|
+
const added = [];
|
|
20
|
+
const skipped = [];
|
|
21
|
+
if (!settings.hooks) {
|
|
22
|
+
settings.hooks = {};
|
|
23
|
+
}
|
|
24
|
+
for (const { event, matcher, hook } of input.newHooks) {
|
|
25
|
+
if (!settings.hooks[event]) {
|
|
26
|
+
settings.hooks[event] = [];
|
|
27
|
+
}
|
|
28
|
+
const existingGroups = settings.hooks[event];
|
|
29
|
+
if (isDuplicate(existingGroups, matcher, hook.command)) {
|
|
30
|
+
skipped.push({
|
|
31
|
+
event,
|
|
32
|
+
matcher,
|
|
33
|
+
command: hook.command,
|
|
34
|
+
reason: "Already exists (same event + matcher + command)"
|
|
35
|
+
});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const newGroup = {
|
|
39
|
+
...matcher !== void 0 ? { matcher } : {},
|
|
40
|
+
hooks: [{ type: hook.type, command: hook.command }]
|
|
41
|
+
};
|
|
42
|
+
existingGroups.push(newGroup);
|
|
43
|
+
added.push({ event, matcher, command: hook.command });
|
|
44
|
+
}
|
|
45
|
+
return { settings, added, skipped };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/config/manager.ts
|
|
49
|
+
function detectIndent(raw) {
|
|
50
|
+
const lines = raw.split("\n");
|
|
51
|
+
for (const line of lines.slice(1)) {
|
|
52
|
+
if (line.startsWith(" ")) return " ";
|
|
53
|
+
const match = line.match(/^( +)/);
|
|
54
|
+
if (match) return match[1].length;
|
|
55
|
+
}
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
async function readSettings(path) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await readFile(path, "utf8");
|
|
61
|
+
return JSON.parse(raw);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function writeSettings(path, settings, originalRaw) {
|
|
70
|
+
const indent = originalRaw ? detectIndent(originalRaw) : 2;
|
|
71
|
+
const output = JSON.stringify(settings, null, indent) + "\n";
|
|
72
|
+
await writeFile(path, output, "utf8");
|
|
73
|
+
}
|
|
74
|
+
async function applyMerge(opts) {
|
|
75
|
+
const { settingsPath, newHooks, dryRun = false } = opts;
|
|
76
|
+
let originalRaw;
|
|
77
|
+
let existing;
|
|
78
|
+
try {
|
|
79
|
+
originalRaw = await readFile(settingsPath, "utf8");
|
|
80
|
+
existing = JSON.parse(originalRaw);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
83
|
+
existing = {};
|
|
84
|
+
originalRaw = void 0;
|
|
85
|
+
} else {
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const result = mergeHooks({ existing, newHooks });
|
|
90
|
+
if (!dryRun) {
|
|
91
|
+
await createBackup(settingsPath);
|
|
92
|
+
await writeSettings(settingsPath, result.settings, originalRaw);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
readSettings,
|
|
99
|
+
writeSettings,
|
|
100
|
+
applyMerge
|
|
101
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config/locator.ts
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join, resolve, dirname } from "path";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
function findProjectRoot() {
|
|
8
|
+
const home = homedir();
|
|
9
|
+
let dir = resolve(process.cwd());
|
|
10
|
+
while (true) {
|
|
11
|
+
if (existsSync(join(dir, ".git"))) {
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
if (dir !== home && existsSync(join(dir, ".claude"))) {
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
const parent = dirname(dir);
|
|
18
|
+
if (parent === dir) break;
|
|
19
|
+
dir = parent;
|
|
20
|
+
}
|
|
21
|
+
return process.cwd();
|
|
22
|
+
}
|
|
23
|
+
function getSettingsPath(scope) {
|
|
24
|
+
switch (scope) {
|
|
25
|
+
case "user":
|
|
26
|
+
return join(homedir(), ".claude", "settings.json");
|
|
27
|
+
case "project":
|
|
28
|
+
return resolve(findProjectRoot(), ".claude", "settings.json");
|
|
29
|
+
case "local":
|
|
30
|
+
return resolve(findProjectRoot(), ".claude", "settings.local.json");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getHooksDir(scope) {
|
|
34
|
+
switch (scope) {
|
|
35
|
+
case "user":
|
|
36
|
+
return join(homedir(), ".claude", "hooks");
|
|
37
|
+
case "project":
|
|
38
|
+
case "local":
|
|
39
|
+
return resolve(findProjectRoot(), ".claude", "hooks");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
getSettingsPath,
|
|
45
|
+
getHooksDir
|
|
46
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config/backup.ts
|
|
4
|
+
import { copyFile, access } from "fs/promises";
|
|
5
|
+
import { constants } from "fs";
|
|
6
|
+
async function createBackup(settingsPath) {
|
|
7
|
+
const backupPath = settingsPath + ".backup";
|
|
8
|
+
try {
|
|
9
|
+
await access(settingsPath, constants.F_OK);
|
|
10
|
+
await copyFile(settingsPath, backupPath);
|
|
11
|
+
return backupPath;
|
|
12
|
+
} catch {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function restoreBackup(settingsPath) {
|
|
17
|
+
const backupPath = settingsPath + ".backup";
|
|
18
|
+
try {
|
|
19
|
+
await access(backupPath, constants.F_OK);
|
|
20
|
+
await copyFile(backupPath, settingsPath);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
createBackup,
|
|
29
|
+
restoreBackup
|
|
30
|
+
};
|