@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 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
@@ -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
+ }