@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.
- package/.claude/settings.json +24 -0
- package/bin/index.js +758 -319
- package/dist/index.js +156 -1
- package/hooks/hook-autoformat/README.md +39 -0
- package/hooks/hook-autoformat/package.json +58 -0
- package/hooks/hook-autoformat/src/hook.ts +223 -0
- package/hooks/hook-autostage/README.md +70 -0
- package/hooks/hook-autostage/package.json +12 -0
- package/hooks/hook-autostage/src/hook.ts +167 -0
- package/hooks/hook-commandlog/README.md +45 -0
- package/hooks/hook-commandlog/package.json +12 -0
- package/hooks/hook-commandlog/src/hook.ts +92 -0
- package/hooks/hook-costwatch/README.md +61 -0
- package/hooks/hook-costwatch/package.json +12 -0
- package/hooks/hook-costwatch/src/hook.ts +178 -0
- package/hooks/hook-desktopnotify/README.md +50 -0
- package/hooks/hook-desktopnotify/package.json +57 -0
- package/hooks/hook-desktopnotify/src/hook.ts +112 -0
- package/hooks/hook-envsetup/README.md +40 -0
- package/hooks/hook-envsetup/package.json +58 -0
- package/hooks/hook-envsetup/src/hook.ts +197 -0
- package/hooks/hook-errornotify/README.md +66 -0
- package/hooks/hook-errornotify/package.json +12 -0
- package/hooks/hook-errornotify/src/hook.ts +197 -0
- package/hooks/hook-permissionguard/README.md +48 -0
- package/hooks/hook-permissionguard/package.json +58 -0
- package/hooks/hook-permissionguard/src/hook.ts +268 -0
- package/hooks/hook-promptguard/README.md +64 -0
- package/hooks/hook-promptguard/package.json +12 -0
- package/hooks/hook-promptguard/src/hook.ts +200 -0
- package/hooks/hook-protectfiles/README.md +62 -0
- package/hooks/hook-protectfiles/package.json +58 -0
- package/hooks/hook-protectfiles/src/hook.ts +267 -0
- package/hooks/hook-sessionlog/README.md +48 -0
- package/hooks/hook-sessionlog/package.json +12 -0
- package/hooks/hook-sessionlog/src/hook.ts +100 -0
- package/hooks/hook-slacknotify/README.md +62 -0
- package/hooks/hook-slacknotify/package.json +12 -0
- package/hooks/hook-slacknotify/src/hook.ts +146 -0
- package/hooks/hook-soundnotify/README.md +63 -0
- package/hooks/hook-soundnotify/package.json +12 -0
- package/hooks/hook-soundnotify/src/hook.ts +173 -0
- package/hooks/hook-taskgate/README.md +62 -0
- package/hooks/hook-taskgate/package.json +12 -0
- package/hooks/hook-taskgate/src/hook.ts +169 -0
- package/hooks/hook-tddguard/README.md +50 -0
- package/hooks/hook-tddguard/package.json +12 -0
- package/hooks/hook-tddguard/src/hook.ts +263 -0
- 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
|
+
}
|