@hasna/hooks 0.0.6 → 0.1.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.
Files changed (49) hide show
  1. package/.claude/settings.json +24 -0
  2. package/bin/index.js +758 -319
  3. package/dist/index.js +156 -1
  4. package/hooks/hook-autoformat/README.md +39 -0
  5. package/hooks/hook-autoformat/package.json +58 -0
  6. package/hooks/hook-autoformat/src/hook.ts +223 -0
  7. package/hooks/hook-autostage/README.md +70 -0
  8. package/hooks/hook-autostage/package.json +12 -0
  9. package/hooks/hook-autostage/src/hook.ts +167 -0
  10. package/hooks/hook-commandlog/README.md +45 -0
  11. package/hooks/hook-commandlog/package.json +12 -0
  12. package/hooks/hook-commandlog/src/hook.ts +92 -0
  13. package/hooks/hook-costwatch/README.md +61 -0
  14. package/hooks/hook-costwatch/package.json +12 -0
  15. package/hooks/hook-costwatch/src/hook.ts +178 -0
  16. package/hooks/hook-desktopnotify/README.md +50 -0
  17. package/hooks/hook-desktopnotify/package.json +57 -0
  18. package/hooks/hook-desktopnotify/src/hook.ts +112 -0
  19. package/hooks/hook-envsetup/README.md +40 -0
  20. package/hooks/hook-envsetup/package.json +58 -0
  21. package/hooks/hook-envsetup/src/hook.ts +197 -0
  22. package/hooks/hook-errornotify/README.md +66 -0
  23. package/hooks/hook-errornotify/package.json +12 -0
  24. package/hooks/hook-errornotify/src/hook.ts +197 -0
  25. package/hooks/hook-permissionguard/README.md +48 -0
  26. package/hooks/hook-permissionguard/package.json +58 -0
  27. package/hooks/hook-permissionguard/src/hook.ts +268 -0
  28. package/hooks/hook-promptguard/README.md +64 -0
  29. package/hooks/hook-promptguard/package.json +12 -0
  30. package/hooks/hook-promptguard/src/hook.ts +200 -0
  31. package/hooks/hook-protectfiles/README.md +62 -0
  32. package/hooks/hook-protectfiles/package.json +58 -0
  33. package/hooks/hook-protectfiles/src/hook.ts +267 -0
  34. package/hooks/hook-sessionlog/README.md +48 -0
  35. package/hooks/hook-sessionlog/package.json +12 -0
  36. package/hooks/hook-sessionlog/src/hook.ts +100 -0
  37. package/hooks/hook-slacknotify/README.md +62 -0
  38. package/hooks/hook-slacknotify/package.json +12 -0
  39. package/hooks/hook-slacknotify/src/hook.ts +146 -0
  40. package/hooks/hook-soundnotify/README.md +63 -0
  41. package/hooks/hook-soundnotify/package.json +12 -0
  42. package/hooks/hook-soundnotify/src/hook.ts +173 -0
  43. package/hooks/hook-taskgate/README.md +62 -0
  44. package/hooks/hook-taskgate/package.json +12 -0
  45. package/hooks/hook-taskgate/src/hook.ts +169 -0
  46. package/hooks/hook-tddguard/README.md +50 -0
  47. package/hooks/hook-tddguard/package.json +12 -0
  48. package/hooks/hook-tddguard/src/hook.ts +263 -0
  49. package/package.json +4 -3
@@ -0,0 +1,62 @@
1
+ # hook-protectfiles
2
+
3
+ Claude Code hook that blocks access to sensitive files like `.env`, secrets, keys, and lock files.
4
+
5
+ ## Overview
6
+
7
+ Prevents Claude from reading or modifying files that contain secrets, credentials, or are auto-generated (lock files). Protects across all tool types — file operations and bash commands.
8
+
9
+ ## Hook Event
10
+
11
+ - **PreToolUse** (matcher: `Edit|Write|Read|Bash`)
12
+
13
+ ## Protected Files
14
+
15
+ ### Always Blocked (Read + Write)
16
+
17
+ | Pattern | Description |
18
+ |---------|-------------|
19
+ | `.env`, `.env.*` | Environment variable files |
20
+ | `.secrets/` | Secrets directory |
21
+ | `credentials.json` | Credential files |
22
+ | `*.pem`, `*.key`, `*.p12`, `*.pfx` | SSL/TLS certificates and keys |
23
+ | `id_rsa`, `id_ed25519`, `id_ecdsa` | SSH keys |
24
+ | `.ssh/` | SSH directory |
25
+ | `.aws/credentials` | AWS credentials |
26
+ | `.npmrc` | npm config (may contain tokens) |
27
+ | `.netrc` | Network credentials |
28
+ | `*.keystore`, `*.jks` | Java keystores |
29
+
30
+ ### Write-Only Block (Read is OK)
31
+
32
+ | Pattern | Description |
33
+ |---------|-------------|
34
+ | `package-lock.json` | npm lock file |
35
+ | `yarn.lock` | Yarn lock file |
36
+ | `bun.lock`, `bun.lockb` | Bun lock files |
37
+ | `pnpm-lock.yaml` | pnpm lock file |
38
+ | `Gemfile.lock` | Ruby lock file |
39
+ | `poetry.lock` | Poetry lock file |
40
+ | `Cargo.lock` | Rust lock file |
41
+ | `composer.lock` | PHP lock file |
42
+
43
+ ## Tool Coverage
44
+
45
+ | Tool | Check Method |
46
+ |------|-------------|
47
+ | `Read` | Checks `tool_input.file_path` against protected patterns |
48
+ | `Edit` | Checks `tool_input.file_path` against protected + lock patterns |
49
+ | `Write` | Checks `tool_input.file_path` against protected + lock patterns |
50
+ | `Bash` | Scans command string for references to protected files |
51
+
52
+ ## Bash Command Intelligence
53
+
54
+ For Bash commands, the hook:
55
+ - Allows git commands that naturally reference `.env` (e.g., `git add .gitignore` where `.env` appears)
56
+ - Blocks direct file access (`cat .env`, `cp .secrets/`, etc.)
57
+ - Blocks redirects to lock files (`> package-lock.json`)
58
+ - Blocks sed/awk modifications to lock files
59
+
60
+ ## License
61
+
62
+ MIT
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@hasna/hook-protectfiles",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that blocks access to sensitive files like .env, secrets, and keys",
5
+ "type": "module",
6
+ "bin": {
7
+ "hook-protectfiles": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/hook.js",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/hook.js",
13
+ "types": "./dist/hook.d.ts"
14
+ },
15
+ "./cli": {
16
+ "import": "./dist/cli.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "bun build ./src/hook.ts --outdir ./dist --target node",
25
+ "prepublishOnly": "bun run build",
26
+ "typecheck": "tsc --noEmit"
27
+ },
28
+ "keywords": [
29
+ "claude-code",
30
+ "claude",
31
+ "hook",
32
+ "security",
33
+ "protect",
34
+ "secrets",
35
+ "env",
36
+ "credentials",
37
+ "cli"
38
+ ],
39
+ "author": "Hasna",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/hasna/open-hooks.git"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "registry": "https://registry.npmjs.org/"
48
+ },
49
+ "engines": {
50
+ "node": ">=18",
51
+ "bun": ">=1.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/bun": "^1.3.8",
55
+ "@types/node": "^20",
56
+ "typescript": "^5.0.0"
57
+ }
58
+ }
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: protectfiles
5
+ *
6
+ * PreToolUse hook that blocks access to sensitive files:
7
+ *
8
+ * Always blocked (Edit/Write/Read/Bash):
9
+ * - .env, .env.local, .env.production, .env.*
10
+ * - .secrets/, credentials.json
11
+ * - *.pem, *.key
12
+ * - id_rsa, id_ed25519, .ssh/
13
+ *
14
+ * Blocked for Edit/Write only (Read is OK):
15
+ * - package-lock.json, yarn.lock, bun.lock, bun.lockb
16
+ *
17
+ * For Edit/Write/Read: checks tool_input.file_path
18
+ * For Bash: checks if the command references protected files
19
+ */
20
+
21
+ import { readFileSync } from "fs";
22
+ import { basename } from "path";
23
+
24
+ interface HookInput {
25
+ session_id: string;
26
+ cwd: string;
27
+ tool_name: string;
28
+ tool_input: Record<string, unknown>;
29
+ }
30
+
31
+ interface HookOutput {
32
+ decision: "approve" | "block";
33
+ reason?: string;
34
+ }
35
+
36
+ function readStdinJson(): HookInput | null {
37
+ try {
38
+ const input = readFileSync(0, "utf-8").trim();
39
+ if (!input) return null;
40
+ return JSON.parse(input);
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function respond(output: HookOutput): void {
47
+ console.log(JSON.stringify(output));
48
+ }
49
+
50
+ /**
51
+ * Sensitive file patterns that are always blocked (read + write).
52
+ */
53
+ const ALWAYS_PROTECTED_PATTERNS: Array<{ pattern: RegExp; description: string }> = [
54
+ // Environment files
55
+ { pattern: /(?:^|\/)\.env$/, description: ".env file" },
56
+ { pattern: /(?:^|\/)\.env\.[a-zA-Z0-9._-]+$/, description: ".env.* file" },
57
+
58
+ // Secrets directory
59
+ { pattern: /(?:^|\/)\.secrets(?:\/|$)/, description: ".secrets/ directory" },
60
+
61
+ // Credential files
62
+ { pattern: /(?:^|\/)credentials\.json$/, description: "credentials.json" },
63
+
64
+ // SSL/TLS keys and certificates
65
+ { pattern: /\.pem$/, description: ".pem file (certificate/key)" },
66
+ { pattern: /\.key$/, description: ".key file (private key)" },
67
+ { pattern: /\.p12$/, description: ".p12 file (certificate bundle)" },
68
+ { pattern: /\.pfx$/, description: ".pfx file (certificate bundle)" },
69
+
70
+ // SSH keys
71
+ { pattern: /(?:^|\/)id_rsa(?:\.pub)?$/, description: "SSH RSA key" },
72
+ { pattern: /(?:^|\/)id_ed25519(?:\.pub)?$/, description: "SSH Ed25519 key" },
73
+ { pattern: /(?:^|\/)id_ecdsa(?:\.pub)?$/, description: "SSH ECDSA key" },
74
+ { pattern: /(?:^|\/)id_dsa(?:\.pub)?$/, description: "SSH DSA key" },
75
+ { pattern: /(?:^|\/)\.ssh\//, description: ".ssh/ directory" },
76
+
77
+ // AWS credentials
78
+ { pattern: /(?:^|\/)\.aws\/credentials$/, description: "AWS credentials" },
79
+
80
+ // Token files
81
+ { pattern: /(?:^|\/)\.npmrc$/, description: ".npmrc (may contain tokens)" },
82
+ { pattern: /(?:^|\/)\.netrc$/, description: ".netrc (may contain credentials)" },
83
+
84
+ // Keystore files
85
+ { pattern: /\.keystore$/, description: "keystore file" },
86
+ { pattern: /\.jks$/, description: "Java keystore" },
87
+ ];
88
+
89
+ /**
90
+ * Lock file patterns — blocked for Edit/Write only, allowed for Read.
91
+ */
92
+ const LOCK_FILE_PATTERNS: Array<{ pattern: RegExp; description: string }> = [
93
+ { pattern: /(?:^|\/)package-lock\.json$/, description: "package-lock.json (auto-generated)" },
94
+ { pattern: /(?:^|\/)yarn\.lock$/, description: "yarn.lock (auto-generated)" },
95
+ { pattern: /(?:^|\/)bun\.lock$/, description: "bun.lock (auto-generated)" },
96
+ { pattern: /(?:^|\/)bun\.lockb$/, description: "bun.lockb (auto-generated binary)" },
97
+ { pattern: /(?:^|\/)pnpm-lock\.yaml$/, description: "pnpm-lock.yaml (auto-generated)" },
98
+ { pattern: /(?:^|\/)Gemfile\.lock$/, description: "Gemfile.lock (auto-generated)" },
99
+ { pattern: /(?:^|\/)poetry\.lock$/, description: "poetry.lock (auto-generated)" },
100
+ { pattern: /(?:^|\/)Cargo\.lock$/, description: "Cargo.lock (auto-generated)" },
101
+ { pattern: /(?:^|\/)composer\.lock$/, description: "composer.lock (auto-generated)" },
102
+ ];
103
+
104
+ type ToolCategory = "read" | "write" | "bash";
105
+
106
+ function getToolCategory(toolName: string): ToolCategory | null {
107
+ switch (toolName) {
108
+ case "Read":
109
+ return "read";
110
+ case "Edit":
111
+ case "Write":
112
+ return "write";
113
+ case "Bash":
114
+ return "bash";
115
+ default:
116
+ return null;
117
+ }
118
+ }
119
+
120
+ function checkFilePath(filePath: string, category: ToolCategory): { blocked: boolean; reason?: string } {
121
+ // Check always-protected files
122
+ for (const { pattern, description } of ALWAYS_PROTECTED_PATTERNS) {
123
+ if (pattern.test(filePath)) {
124
+ return {
125
+ blocked: true,
126
+ reason: `Blocked: access to ${description} (${basename(filePath)})`,
127
+ };
128
+ }
129
+ }
130
+
131
+ // Check lock files — only block writes, allow reads
132
+ if (category === "write") {
133
+ for (const { pattern, description } of LOCK_FILE_PATTERNS) {
134
+ if (pattern.test(filePath)) {
135
+ return {
136
+ blocked: true,
137
+ reason: `Blocked: writing to ${description} — this file is auto-generated`,
138
+ };
139
+ }
140
+ }
141
+ }
142
+
143
+ return { blocked: false };
144
+ }
145
+
146
+ function checkBashCommand(command: string): { blocked: boolean; reason?: string } {
147
+ // For Bash, check if the command references any protected file
148
+ // We check both always-protected and lock files (since bash could write to them)
149
+
150
+ for (const { pattern, description } of ALWAYS_PROTECTED_PATTERNS) {
151
+ // Extract the core pattern to search in the command string
152
+ if (pattern.test(command)) {
153
+ return {
154
+ blocked: true,
155
+ reason: `Blocked: command references ${description}`,
156
+ };
157
+ }
158
+ }
159
+
160
+ // Additional string-based checks for common patterns in bash commands
161
+ const sensitiveReferences: Array<{ pattern: RegExp; description: string }> = [
162
+ { pattern: /\b\.env\b(?!\.)/, description: ".env file" },
163
+ { pattern: /\.env\.[a-zA-Z]+/, description: ".env.* file" },
164
+ { pattern: /\.secrets\//, description: ".secrets/ directory" },
165
+ { pattern: /credentials\.json/, description: "credentials.json" },
166
+ { pattern: /\bid_rsa\b/, description: "SSH RSA key" },
167
+ { pattern: /\bid_ed25519\b/, description: "SSH Ed25519 key" },
168
+ { pattern: /\.ssh\//, description: ".ssh/ directory" },
169
+ { pattern: /\.aws\/credentials/, description: "AWS credentials" },
170
+ ];
171
+
172
+ for (const { pattern, description } of sensitiveReferences) {
173
+ if (pattern.test(command)) {
174
+ // Allow read-only commands that just check existence or list
175
+ // e.g., "test -f .env", "ls .secrets/", "cat .env" should still be caught
176
+ // But git commands that reference .env in .gitignore context are OK
177
+ if (/\bgit\s+(add|commit|diff|status|log)\b/.test(command)) {
178
+ continue;
179
+ }
180
+
181
+ return {
182
+ blocked: true,
183
+ reason: `Blocked: command references ${description}`,
184
+ };
185
+ }
186
+ }
187
+
188
+ // Check if bash command writes to lock files
189
+ const lockFileWritePatterns: Array<{ pattern: RegExp; description: string }> = [
190
+ { pattern: />\s*package-lock\.json/, description: "writing to package-lock.json" },
191
+ { pattern: />\s*yarn\.lock/, description: "writing to yarn.lock" },
192
+ { pattern: />\s*bun\.lock/, description: "writing to bun.lock" },
193
+ { pattern: /sed\s+.*package-lock\.json/, description: "modifying package-lock.json" },
194
+ { pattern: /sed\s+.*yarn\.lock/, description: "modifying yarn.lock" },
195
+ { pattern: /sed\s+.*bun\.lock/, description: "modifying bun.lock" },
196
+ ];
197
+
198
+ for (const { pattern, description } of lockFileWritePatterns) {
199
+ if (pattern.test(command)) {
200
+ return {
201
+ blocked: true,
202
+ reason: `Blocked: ${description} — this file is auto-generated`,
203
+ };
204
+ }
205
+ }
206
+
207
+ return { blocked: false };
208
+ }
209
+
210
+ export function run(): void {
211
+ const input = readStdinJson();
212
+
213
+ if (!input) {
214
+ respond({ decision: "approve" });
215
+ return;
216
+ }
217
+
218
+ const category = getToolCategory(input.tool_name);
219
+ if (!category) {
220
+ respond({ decision: "approve" });
221
+ return;
222
+ }
223
+
224
+ // For Edit/Write/Read: check file_path
225
+ if (category === "read" || category === "write") {
226
+ const filePath = input.tool_input?.file_path as string;
227
+ if (!filePath || typeof filePath !== "string") {
228
+ respond({ decision: "approve" });
229
+ return;
230
+ }
231
+
232
+ const result = checkFilePath(filePath, category);
233
+ if (result.blocked) {
234
+ console.error(`[hook-protectfiles] ${result.reason}`);
235
+ respond({ decision: "block", reason: result.reason });
236
+ return;
237
+ }
238
+
239
+ respond({ decision: "approve" });
240
+ return;
241
+ }
242
+
243
+ // For Bash: check command
244
+ if (category === "bash") {
245
+ const command = input.tool_input?.command as string;
246
+ if (!command || typeof command !== "string") {
247
+ respond({ decision: "approve" });
248
+ return;
249
+ }
250
+
251
+ const result = checkBashCommand(command);
252
+ if (result.blocked) {
253
+ console.error(`[hook-protectfiles] ${result.reason}`);
254
+ respond({ decision: "block", reason: result.reason });
255
+ return;
256
+ }
257
+
258
+ respond({ decision: "approve" });
259
+ return;
260
+ }
261
+
262
+ respond({ decision: "approve" });
263
+ }
264
+
265
+ if (import.meta.main) {
266
+ run();
267
+ }
@@ -0,0 +1,48 @@
1
+ # hook-sessionlog
2
+
3
+ Claude Code hook that logs every tool call to a session log file.
4
+
5
+ ## Overview
6
+
7
+ Every time Claude calls any tool, this hook appends a JSON line to `.claude/session-log-<date>.jsonl` in the project directory. Useful for auditing, debugging, and understanding what Claude did during a session.
8
+
9
+ ## Event
10
+
11
+ - **PostToolUse** (matches all tools)
12
+
13
+ ## Log Format
14
+
15
+ Each line in the `.jsonl` file is a JSON object:
16
+
17
+ ```json
18
+ {
19
+ "timestamp": "2026-02-14T10:30:00.000Z",
20
+ "tool_name": "Edit",
21
+ "tool_input": "{\"file_path\":\"src/index.ts\",\"old_string\":\"...\",\"new_string\":\"...\"}",
22
+ "session_id": "abc123"
23
+ }
24
+ ```
25
+
26
+ - `tool_input` is truncated to 500 characters to keep log files manageable
27
+ - One file per day: `.claude/session-log-2026-02-14.jsonl`
28
+
29
+ ## Behavior
30
+
31
+ - Creates `.claude/` directory if it does not exist
32
+ - Appends to the log file (never overwrites)
33
+ - Non-blocking: logging failures are logged to stderr but never interrupt Claude
34
+ - Outputs `{ "continue": true }` always
35
+
36
+ ## Log Location
37
+
38
+ ```
39
+ <project-root>/
40
+ └── .claude/
41
+ ├── session-log-2026-02-14.jsonl
42
+ ├── session-log-2026-02-15.jsonl
43
+ └── ...
44
+ ```
45
+
46
+ ## License
47
+
48
+ MIT
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "hook-sessionlog",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that logs every tool call to a session log file",
5
+ "type": "module",
6
+ "main": "./src/hook.ts",
7
+ "scripts": {
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "author": "Hasna",
11
+ "license": "MIT"
12
+ }
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Claude Code Hook: sessionlog
5
+ *
6
+ * PostToolUse hook that logs every tool call to a session log file.
7
+ * Creates .claude/session-log-<date>.jsonl in the project directory.
8
+ *
9
+ * Each line is a JSON object with:
10
+ * - timestamp: ISO string
11
+ * - tool_name: name of the tool that was called
12
+ * - tool_input: first 500 characters of the stringified tool input
13
+ * - session_id: current session ID
14
+ */
15
+
16
+ import { readFileSync, existsSync, mkdirSync, appendFileSync } from "fs";
17
+ import { join } from "path";
18
+
19
+ interface HookInput {
20
+ session_id: string;
21
+ cwd: string;
22
+ tool_name: string;
23
+ tool_input: Record<string, unknown>;
24
+ }
25
+
26
+ interface HookOutput {
27
+ continue: boolean;
28
+ }
29
+
30
+ function readStdinJson(): HookInput | null {
31
+ try {
32
+ const input = readFileSync(0, "utf-8").trim();
33
+ if (!input) return null;
34
+ return JSON.parse(input);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function respond(output: HookOutput): void {
41
+ console.log(JSON.stringify(output));
42
+ }
43
+
44
+ function getDateString(): string {
45
+ const now = new Date();
46
+ const year = now.getFullYear();
47
+ const month = String(now.getMonth() + 1).padStart(2, "0");
48
+ const day = String(now.getDate()).padStart(2, "0");
49
+ return `${year}-${month}-${day}`;
50
+ }
51
+
52
+ function truncate(str: string, maxLength: number): string {
53
+ if (str.length <= maxLength) return str;
54
+ return str.slice(0, maxLength) + "...";
55
+ }
56
+
57
+ function logToolCall(input: HookInput): void {
58
+ const claudeDir = join(input.cwd, ".claude");
59
+
60
+ // Create .claude/ directory if it doesn't exist
61
+ if (!existsSync(claudeDir)) {
62
+ mkdirSync(claudeDir, { recursive: true });
63
+ }
64
+
65
+ const dateStr = getDateString();
66
+ const logFile = join(claudeDir, `session-log-${dateStr}.jsonl`);
67
+
68
+ const toolInputStr = truncate(JSON.stringify(input.tool_input), 500);
69
+
70
+ const logEntry = {
71
+ timestamp: new Date().toISOString(),
72
+ tool_name: input.tool_name,
73
+ tool_input: toolInputStr,
74
+ session_id: input.session_id,
75
+ };
76
+
77
+ appendFileSync(logFile, JSON.stringify(logEntry) + "\n");
78
+ }
79
+
80
+ export function run(): void {
81
+ const input = readStdinJson();
82
+
83
+ if (!input) {
84
+ respond({ continue: true });
85
+ return;
86
+ }
87
+
88
+ try {
89
+ logToolCall(input);
90
+ } catch (error) {
91
+ const errMsg = error instanceof Error ? error.message : String(error);
92
+ console.error(`[hook-sessionlog] Warning: failed to log tool call: ${errMsg}`);
93
+ }
94
+
95
+ respond({ continue: true });
96
+ }
97
+
98
+ if (import.meta.main) {
99
+ run();
100
+ }
@@ -0,0 +1,62 @@
1
+ # hook-slacknotify
2
+
3
+ Claude Code hook that sends a Slack webhook notification when Claude Code finishes working.
4
+
5
+ ## Overview
6
+
7
+ When Claude stops, this hook sends a notification to a configured Slack channel via an incoming webhook URL. Useful for being notified when long-running tasks complete.
8
+
9
+ ## Event
10
+
11
+ - **Stop** (no matcher)
12
+
13
+ ## Configuration
14
+
15
+ Configure the webhook URL via one of:
16
+
17
+ ### 1. Environment Variable (preferred)
18
+
19
+ ```bash
20
+ export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T.../B.../xxx"
21
+ ```
22
+
23
+ ### 2. Settings File
24
+
25
+ Add to `~/.claude/settings.json`:
26
+
27
+ ```json
28
+ {
29
+ "slackNotifyConfig": {
30
+ "webhookUrl": "https://hooks.slack.com/services/T.../B.../xxx",
31
+ "enabled": true
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Slack Message Format
37
+
38
+ The hook sends a message with:
39
+
40
+ ```json
41
+ {
42
+ "text": "Claude Code finished in <project-name>",
43
+ "blocks": [{
44
+ "type": "section",
45
+ "text": {
46
+ "type": "mrkdwn",
47
+ "text": "*Claude Code* finished working in `<cwd>`"
48
+ }
49
+ }]
50
+ }
51
+ ```
52
+
53
+ ## Behavior
54
+
55
+ - If no webhook URL is configured, logs a warning to stderr and continues
56
+ - If the webhook request fails, logs the error to stderr and continues
57
+ - Never blocks the session from ending
58
+ - Outputs `{ "continue": true }` always
59
+
60
+ ## License
61
+
62
+ MIT
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "hook-slacknotify",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code hook that sends Slack webhook notifications when Claude stops",
5
+ "type": "module",
6
+ "main": "./src/hook.ts",
7
+ "scripts": {
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "author": "Hasna",
11
+ "license": "MIT"
12
+ }