@cmds-cc/hooks 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/README.md +102 -0
- package/bin/cc-hooks.js +40 -0
- package/examples/spok-api.json +30 -0
- package/lib/fetch.js +42 -0
- package/lib/merge.js +67 -0
- package/lib/prompt.js +37 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# @cmds-cc/hooks
|
|
2
|
+
|
|
3
|
+
Install Claude Code hooks from any GitHub repo.
|
|
4
|
+
|
|
5
|
+
**Docs:** [cmds.cc/hooks](https://cmds.cc/hooks)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @cmds-cc/hooks add sieteunoseis/spok-api
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
1. Fetches `claude-hooks.json` from the target repo
|
|
14
|
+
2. Shows an interactive prompt to select which hooks to install
|
|
15
|
+
3. Merges selected hooks into `~/.claude/settings.json`
|
|
16
|
+
|
|
17
|
+
## Install from hook collections
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Essential safety guardrails
|
|
21
|
+
npx @cmds-cc/hooks add sieteunoseis/hooks.automate.builders/hooks/safety-essentials
|
|
22
|
+
|
|
23
|
+
# Cloud protection (AWS, GCP, Azure)
|
|
24
|
+
npx @cmds-cc/hooks add sieteunoseis/hooks.automate.builders/hooks/cloud-safety
|
|
25
|
+
|
|
26
|
+
# Kubernetes safety
|
|
27
|
+
npx @cmds-cc/hooks add sieteunoseis/hooks.automate.builders/hooks/kubernetes-safety
|
|
28
|
+
|
|
29
|
+
# Cisco UC CLI protection
|
|
30
|
+
npx @cmds-cc/hooks add sieteunoseis/hooks.automate.builders/hooks/cisco-cli-safety
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Browse all collections at [cmds.cc/hooks](https://cmds.cc/hooks).
|
|
34
|
+
|
|
35
|
+
## Install from any repo
|
|
36
|
+
|
|
37
|
+
Any GitHub repo with a `claude-hooks.json` at root is installable:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx @cmds-cc/hooks add owner/repo
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Subdirectories work too:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx @cmds-cc/hooks add owner/repo/path/to/hooks
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## For CLI authors
|
|
50
|
+
|
|
51
|
+
Add a `claude-hooks.json` to your repo root:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"name": "my-tool",
|
|
56
|
+
"description": "Hooks for my CLI tool",
|
|
57
|
+
"author": "your-username",
|
|
58
|
+
"version": "1.0.0",
|
|
59
|
+
"hooks": [
|
|
60
|
+
{
|
|
61
|
+
"name": "Block write operations",
|
|
62
|
+
"description": "Prevents write commands without approval",
|
|
63
|
+
"default": true,
|
|
64
|
+
"event": "PreToolUse",
|
|
65
|
+
"matcher": "Bash",
|
|
66
|
+
"hook": {
|
|
67
|
+
"type": "command",
|
|
68
|
+
"command": "your hook command here"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
See the [docs](https://cmds.cc/hooks/docs) for the full authoring guide.
|
|
76
|
+
|
|
77
|
+
## Telemetry
|
|
78
|
+
|
|
79
|
+
After a successful install, the CLI registers the repo in the [directory](https://cmds.cc/hooks) so others can discover it. Only the repo name is sent — no personal data.
|
|
80
|
+
|
|
81
|
+
Opt out:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx @cmds-cc/hooks add owner/repo --no-telemetry
|
|
85
|
+
CC_HOOKS_NO_TELEMETRY=1 npx @cmds-cc/hooks add owner/repo
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Migrating from cc-hooks-install
|
|
89
|
+
|
|
90
|
+
This package replaces `cc-hooks-install`. Same code, new name:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Old
|
|
94
|
+
npx cc-hooks-install add owner/repo
|
|
95
|
+
|
|
96
|
+
# New
|
|
97
|
+
npx @cmds-cc/hooks add owner/repo
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/bin/cc-hooks.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import { fetchHooks } from "../lib/fetch.js";
|
|
5
|
+
import { selectHooks } from "../lib/prompt.js";
|
|
6
|
+
import { mergeHooks } from "../lib/merge.js";
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name("cmds-hooks")
|
|
10
|
+
.description("Install Claude Code hooks from any GitHub repo — cmds.cc/hooks")
|
|
11
|
+
.version("1.0.0");
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.command("add")
|
|
15
|
+
.argument("<repo>", "GitHub repo (owner/repo)")
|
|
16
|
+
.option("--no-telemetry", "Don't register this install in the directory")
|
|
17
|
+
.description("Install hooks from a GitHub repository")
|
|
18
|
+
.action(async (repo, opts) => {
|
|
19
|
+
try {
|
|
20
|
+
const manifest = await fetchHooks(repo);
|
|
21
|
+
const selected = await selectHooks(manifest);
|
|
22
|
+
const result = await mergeHooks(selected, manifest, repo, {
|
|
23
|
+
noTelemetry: opts.telemetry === false,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(` ✓ ${result.added} hook(s) installed to ${result.path}`);
|
|
28
|
+
if (result.skipped > 0) {
|
|
29
|
+
console.log(
|
|
30
|
+
` ⊘ ${result.skipped} hook(s) skipped (already installed)`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
console.log();
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(`\n ✗ ${err.message}\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program.parse();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spok-api",
|
|
3
|
+
"description": "Hooks for Spok SmartSuite CLI",
|
|
4
|
+
"author": "sieteunoseis",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"name": "Block write operations",
|
|
9
|
+
"description": "Blocks send-page, change-status, add, update, delete, assign, set, datafeed",
|
|
10
|
+
"default": true,
|
|
11
|
+
"event": "PreToolUse",
|
|
12
|
+
"matcher": "Bash",
|
|
13
|
+
"hook": {
|
|
14
|
+
"type": "command",
|
|
15
|
+
"command": "jq -r '.tool_input.command' | { read -r cmd; if echo \"$cmd\" | grep -qE '^(npx )?spok-api (send-page|change-status|add |update |delete |assign |set |datafeed )'; then echo '{\"decision\":\"block\",\"reason\":\"BLOCKED: spok-api write operation. Get explicit user approval.\"}'; fi; }"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "Enforce --read-only flag",
|
|
20
|
+
"description": "Requires --read-only on every spok-api command",
|
|
21
|
+
"default": false,
|
|
22
|
+
"event": "PreToolUse",
|
|
23
|
+
"matcher": "Bash",
|
|
24
|
+
"hook": {
|
|
25
|
+
"type": "command",
|
|
26
|
+
"command": "jq -r '.tool_input.command' | { read -r cmd; if echo \"$cmd\" | grep -qE '^(npx )?spok-api ' && ! echo \"$cmd\" | grep -q '\\-\\-read-only'; then echo '{\"decision\":\"block\",\"reason\":\"BLOCKED: spok-api must be run with --read-only. Retry with the flag.\"}'; fi; }"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
package/lib/fetch.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export async function fetchHooks(repo) {
|
|
2
|
+
const parts = repo.split("/");
|
|
3
|
+
if (parts.length < 2) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
`Invalid repo format: "${repo}". Expected owner/repo or owner/repo/path`,
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const owner = parts[0];
|
|
10
|
+
const name = parts[1];
|
|
11
|
+
const subpath = parts.length > 2 ? parts.slice(2).join("/") + "/" : "";
|
|
12
|
+
|
|
13
|
+
const url = `https://raw.githubusercontent.com/${owner}/${name}/HEAD/${subpath}claude-hooks.json`;
|
|
14
|
+
const res = await fetch(url, {
|
|
15
|
+
headers: { "User-Agent": "cc-hooks-install" },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (res.status === 404) {
|
|
19
|
+
throw new Error(`No claude-hooks.json found in ${repo}`);
|
|
20
|
+
}
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
throw new Error(`GitHub error (${res.status}): ${res.statusText}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const data = await res.json();
|
|
26
|
+
|
|
27
|
+
if (!data.name || !Array.isArray(data.hooks) || data.hooks.length === 0) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Invalid claude-hooks.json in ${repo}: missing name or hooks array`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const hook of data.hooks) {
|
|
34
|
+
if (!hook.name || !hook.event || !hook.matcher || !hook.hook) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Invalid hook entry "${hook.name || "unnamed"}": missing required fields (name, event, matcher, hook)`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return data;
|
|
42
|
+
}
|
package/lib/merge.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
6
|
+
|
|
7
|
+
export async function mergeHooks(selected, manifest, repo, options) {
|
|
8
|
+
let settings;
|
|
9
|
+
try {
|
|
10
|
+
const raw = await readFile(SETTINGS_PATH, "utf8");
|
|
11
|
+
settings = JSON.parse(raw);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
if (err.code === "ENOENT") {
|
|
14
|
+
await mkdir(join(homedir(), ".claude"), { recursive: true });
|
|
15
|
+
settings = {};
|
|
16
|
+
} else {
|
|
17
|
+
throw new Error(`Failed to read ${SETTINGS_PATH}: ${err.message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!settings.hooks) {
|
|
22
|
+
settings.hooks = {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let added = 0;
|
|
26
|
+
let skipped = 0;
|
|
27
|
+
|
|
28
|
+
for (const hook of selected) {
|
|
29
|
+
const event = hook.event;
|
|
30
|
+
if (!settings.hooks[event]) {
|
|
31
|
+
settings.hooks[event] = [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let matcherGroup = settings.hooks[event].find(
|
|
35
|
+
(g) => g.matcher === hook.matcher,
|
|
36
|
+
);
|
|
37
|
+
if (!matcherGroup) {
|
|
38
|
+
matcherGroup = { matcher: hook.matcher, hooks: [] };
|
|
39
|
+
settings.hooks[event].push(matcherGroup);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const cmd = hook.hook.command;
|
|
43
|
+
const isDuplicate = matcherGroup.hooks.some((h) => h.command === cmd);
|
|
44
|
+
if (isDuplicate) {
|
|
45
|
+
skipped++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
matcherGroup.hooks.push(hook.hook);
|
|
50
|
+
added++;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
54
|
+
|
|
55
|
+
// Fire-and-forget registration — never blocks the user
|
|
56
|
+
const noTelemetry =
|
|
57
|
+
options?.noTelemetry || process.env.CC_HOOKS_NO_TELEMETRY === "1";
|
|
58
|
+
if (repo && added > 0 && !noTelemetry) {
|
|
59
|
+
fetch("https://hooks.automate.builders/api/register", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
headers: { "Content-Type": "application/json" },
|
|
62
|
+
body: JSON.stringify({ repo }),
|
|
63
|
+
}).catch(() => {});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { added, skipped, path: SETTINGS_PATH };
|
|
67
|
+
}
|
package/lib/prompt.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { checkbox } from "@inquirer/prompts";
|
|
2
|
+
|
|
3
|
+
const BANNER = `
|
|
4
|
+
██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗███████╗
|
|
5
|
+
██║ ██║██╔═══██╗██╔═══██╗██║ ██╔╝██╔════╝
|
|
6
|
+
███████║██║ ██║██║ ██║█████╔╝ ███████╗
|
|
7
|
+
██╔══██║██║ ██║██║ ██║██╔═██╗ ╚════██║
|
|
8
|
+
██║ ██║╚██████╔╝╚██████╔╝██║ ██╗███████║
|
|
9
|
+
╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝`;
|
|
10
|
+
|
|
11
|
+
export async function selectHooks(manifest) {
|
|
12
|
+
console.log(BANNER);
|
|
13
|
+
console.log();
|
|
14
|
+
console.log(` 📦 ${manifest.name} v${manifest.version}`);
|
|
15
|
+
console.log(` ${manifest.description}`);
|
|
16
|
+
console.log();
|
|
17
|
+
|
|
18
|
+
const choices = manifest.hooks.map((hook) => ({
|
|
19
|
+
name: `${hook.name} (${hook.event} → ${hook.matcher})`,
|
|
20
|
+
value: hook,
|
|
21
|
+
checked: hook.default !== false,
|
|
22
|
+
description: hook.description,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const selected = await checkbox({
|
|
26
|
+
message: "Select hooks to install",
|
|
27
|
+
choices,
|
|
28
|
+
instructions: false,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (selected.length === 0) {
|
|
32
|
+
console.log(" No hooks selected. Nothing to install.");
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return selected;
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cmds-cc/hooks",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Install Claude Code hooks from any GitHub repo",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Jeremy Worden",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/cmds-cc/hooks"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://cmds.cc/hooks",
|
|
12
|
+
"bin": {
|
|
13
|
+
"cmds-hooks": "./bin/cc-hooks.js"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude-code",
|
|
21
|
+
"hooks",
|
|
22
|
+
"cli",
|
|
23
|
+
"safety",
|
|
24
|
+
"guardrails",
|
|
25
|
+
"ai-agents",
|
|
26
|
+
"cmds-cc"
|
|
27
|
+
],
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"lib",
|
|
31
|
+
"examples"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^14.0.0",
|
|
35
|
+
"@inquirer/prompts": "^7.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|