@hasna/hooks 0.0.7 → 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/bin/index.js +157 -2
- 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 +2 -2
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Hook: promptguard
|
|
5
|
+
*
|
|
6
|
+
* UserPromptSubmit hook that validates user prompts before Claude processes them.
|
|
7
|
+
* Blocks prompts containing:
|
|
8
|
+
* - Known prompt injection patterns
|
|
9
|
+
* - Attempts to access credentials
|
|
10
|
+
* - Social engineering attempts
|
|
11
|
+
*
|
|
12
|
+
* All matching is case-insensitive.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
|
|
17
|
+
interface HookInput {
|
|
18
|
+
session_id: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
tool_name: string;
|
|
21
|
+
tool_input: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface HookOutput {
|
|
25
|
+
decision: "approve" | "block";
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Patterns that indicate prompt injection attempts
|
|
31
|
+
*/
|
|
32
|
+
const INJECTION_PATTERNS: RegExp[] = [
|
|
33
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
34
|
+
/ignore\s+(all\s+)?prior\s+instructions/i,
|
|
35
|
+
/ignore\s+(all\s+)?above\s+instructions/i,
|
|
36
|
+
/disregard\s+(all\s+)?previous\s+instructions/i,
|
|
37
|
+
/disregard\s+(all\s+)?prior\s+instructions/i,
|
|
38
|
+
/forget\s+(all\s+)?previous\s+instructions/i,
|
|
39
|
+
/override\s+(all\s+)?previous\s+instructions/i,
|
|
40
|
+
/new\s+system\s+prompt/i,
|
|
41
|
+
/your\s+system\s+prompt/i,
|
|
42
|
+
/reveal\s+(your\s+)?system\s+prompt/i,
|
|
43
|
+
/show\s+(me\s+)?(your\s+)?system\s+prompt/i,
|
|
44
|
+
/print\s+(your\s+)?system\s+prompt/i,
|
|
45
|
+
/output\s+(your\s+)?system\s+prompt/i,
|
|
46
|
+
/what\s+(is|are)\s+your\s+instructions/i,
|
|
47
|
+
/you\s+are\s+now\b/i,
|
|
48
|
+
/from\s+now\s+on\s+you\s+are/i,
|
|
49
|
+
/you\s+have\s+been\s+reprogrammed/i,
|
|
50
|
+
/entering\s+(a\s+)?new\s+mode/i,
|
|
51
|
+
/switch\s+to\s+(\w+\s+)?mode/i,
|
|
52
|
+
/jailbreak/i,
|
|
53
|
+
/DAN\s+mode/i,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Patterns that attempt to access credentials
|
|
58
|
+
*/
|
|
59
|
+
const CREDENTIAL_PATTERNS: RegExp[] = [
|
|
60
|
+
/show\s+(me\s+)?(the\s+)?api\s*key/i,
|
|
61
|
+
/print\s+(the\s+)?api\s*key/i,
|
|
62
|
+
/reveal\s+(the\s+)?api\s*key/i,
|
|
63
|
+
/display\s+(the\s+)?api\s*key/i,
|
|
64
|
+
/output\s+(the\s+)?api\s*key/i,
|
|
65
|
+
/what\s+(is|are)\s+(the\s+)?api\s*key/i,
|
|
66
|
+
/show\s+(me\s+)?(the\s+)?token/i,
|
|
67
|
+
/print\s+(the\s+)?token/i,
|
|
68
|
+
/reveal\s+(the\s+)?token/i,
|
|
69
|
+
/display\s+(the\s+)?token/i,
|
|
70
|
+
/show\s+(me\s+)?(the\s+)?password/i,
|
|
71
|
+
/print\s+(the\s+)?password/i,
|
|
72
|
+
/reveal\s+(the\s+)?password/i,
|
|
73
|
+
/display\s+(the\s+)?password/i,
|
|
74
|
+
/show\s+(me\s+)?(the\s+)?secret/i,
|
|
75
|
+
/print\s+(the\s+)?secret/i,
|
|
76
|
+
/reveal\s+(the\s+)?secret/i,
|
|
77
|
+
/show\s+(me\s+)?(the\s+)?credentials/i,
|
|
78
|
+
/reveal\s+(the\s+)?credentials/i,
|
|
79
|
+
/dump\s+(all\s+)?credentials/i,
|
|
80
|
+
/dump\s+(all\s+)?secrets/i,
|
|
81
|
+
/dump\s+(all\s+)?tokens/i,
|
|
82
|
+
/extract\s+(the\s+)?credentials/i,
|
|
83
|
+
/read\s+\.env\b/i,
|
|
84
|
+
/cat\s+\.env\b/i,
|
|
85
|
+
/cat\s+\.secrets\//i,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Patterns that indicate social engineering attempts
|
|
90
|
+
*/
|
|
91
|
+
const SOCIAL_ENGINEERING_PATTERNS: RegExp[] = [
|
|
92
|
+
/pretend\s+(that\s+)?you\s+are/i,
|
|
93
|
+
/pretend\s+to\s+be/i,
|
|
94
|
+
/act\s+as\s+root/i,
|
|
95
|
+
/act\s+as\s+(an?\s+)?admin/i,
|
|
96
|
+
/act\s+as\s+(an?\s+)?administrator/i,
|
|
97
|
+
/sudo\s+mode/i,
|
|
98
|
+
/admin\s+mode/i,
|
|
99
|
+
/root\s+mode/i,
|
|
100
|
+
/god\s+mode/i,
|
|
101
|
+
/developer\s+mode/i,
|
|
102
|
+
/maintenance\s+mode/i,
|
|
103
|
+
/debug\s+mode/i,
|
|
104
|
+
/unrestricted\s+mode/i,
|
|
105
|
+
/bypass\s+(all\s+)?restrictions/i,
|
|
106
|
+
/bypass\s+(all\s+)?safety/i,
|
|
107
|
+
/bypass\s+(all\s+)?filters/i,
|
|
108
|
+
/disable\s+(all\s+)?safety/i,
|
|
109
|
+
/disable\s+(all\s+)?restrictions/i,
|
|
110
|
+
/remove\s+(all\s+)?restrictions/i,
|
|
111
|
+
/remove\s+(all\s+)?safety/i,
|
|
112
|
+
/turn\s+off\s+(all\s+)?safety/i,
|
|
113
|
+
/turn\s+off\s+(all\s+)?restrictions/i,
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read and parse JSON from stdin
|
|
118
|
+
*/
|
|
119
|
+
function readStdinJson(): HookInput | null {
|
|
120
|
+
try {
|
|
121
|
+
const input = readFileSync(0, "utf-8").trim();
|
|
122
|
+
if (!input) return null;
|
|
123
|
+
return JSON.parse(input);
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check a prompt against all pattern lists
|
|
131
|
+
*/
|
|
132
|
+
function checkPrompt(prompt: string): { blocked: boolean; category?: string } {
|
|
133
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
134
|
+
if (pattern.test(prompt)) {
|
|
135
|
+
return { blocked: true, category: "prompt injection" };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const pattern of CREDENTIAL_PATTERNS) {
|
|
140
|
+
if (pattern.test(prompt)) {
|
|
141
|
+
return { blocked: true, category: "credential access attempt" };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const pattern of SOCIAL_ENGINEERING_PATTERNS) {
|
|
146
|
+
if (pattern.test(prompt)) {
|
|
147
|
+
return { blocked: true, category: "social engineering" };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { blocked: false };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Output hook response
|
|
156
|
+
*/
|
|
157
|
+
function respond(output: HookOutput): void {
|
|
158
|
+
console.log(JSON.stringify(output));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Main hook execution
|
|
163
|
+
*/
|
|
164
|
+
export function run(): void {
|
|
165
|
+
const input = readStdinJson();
|
|
166
|
+
|
|
167
|
+
if (!input) {
|
|
168
|
+
respond({ decision: "approve" });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Extract prompt text from tool_input
|
|
173
|
+
const prompt =
|
|
174
|
+
(input.tool_input?.prompt as string) ||
|
|
175
|
+
(input.tool_input?.content as string) ||
|
|
176
|
+
(input.tool_input?.message as string) ||
|
|
177
|
+
"";
|
|
178
|
+
|
|
179
|
+
if (!prompt || typeof prompt !== "string") {
|
|
180
|
+
respond({ decision: "approve" });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const result = checkPrompt(prompt);
|
|
185
|
+
|
|
186
|
+
if (result.blocked) {
|
|
187
|
+
console.error(`[hook-promptguard] Blocked: potential ${result.category}`);
|
|
188
|
+
respond({
|
|
189
|
+
decision: "block",
|
|
190
|
+
reason: "Blocked: potential prompt injection",
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
respond({ decision: "approve" });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (import.meta.main) {
|
|
199
|
+
run();
|
|
200
|
+
}
|
|
@@ -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
|
+
}
|