@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,48 @@
|
|
|
1
|
+
# hook-permissionguard
|
|
2
|
+
|
|
3
|
+
Claude Code hook that auto-approves safe read-only commands and blocks dangerous patterns.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Reduces permission prompts for safe commands while blocking truly dangerous operations. Commands that don't match either list pass through normally.
|
|
8
|
+
|
|
9
|
+
## Hook Event
|
|
10
|
+
|
|
11
|
+
- **PreToolUse** (matcher: `Bash`)
|
|
12
|
+
|
|
13
|
+
## Auto-Approved Commands
|
|
14
|
+
|
|
15
|
+
Read-only commands that are always safe:
|
|
16
|
+
|
|
17
|
+
| Category | Commands |
|
|
18
|
+
|----------|----------|
|
|
19
|
+
| Git (read-only) | `git status`, `git log`, `git diff`, `git branch`, `git show`, `git tag` |
|
|
20
|
+
| File reading | `ls`, `cat`, `head`, `tail`, `wc`, `find`, `grep`, `rg`, `pwd` |
|
|
21
|
+
| Testing | `npm test`, `bun test`, `pytest`, `cargo test`, `go test`, `jest`, `vitest` |
|
|
22
|
+
| Package listing | `npm list`, `bun pm ls`, `pip list`, `cargo tree` |
|
|
23
|
+
| Version checks | `node -v`, `bun -v`, `python --version`, `cargo --version`, etc. |
|
|
24
|
+
|
|
25
|
+
**Note**: Piped commands (`cmd | cmd`), chained commands (`cmd && cmd`), and semicolon-separated commands (`cmd; cmd`) are never auto-approved, even if individual parts are safe.
|
|
26
|
+
|
|
27
|
+
## Blocked Commands
|
|
28
|
+
|
|
29
|
+
Dangerous patterns that are always blocked:
|
|
30
|
+
|
|
31
|
+
| Pattern | Reason |
|
|
32
|
+
|---------|--------|
|
|
33
|
+
| `rm -rf /`, `rm -rf ~`, `rm -rf $HOME` | Destructive deletion |
|
|
34
|
+
| `:(){ :\|:& };:` | Fork bomb |
|
|
35
|
+
| `dd if=`, `mkfs.`, `fdisk` | Disk destruction |
|
|
36
|
+
| `curl \| sh`, `wget \| sh` | Remote code execution |
|
|
37
|
+
| `chmod 777`, `chmod -R 777` | Insecure permissions |
|
|
38
|
+
| `shutdown`, `reboot` | System control |
|
|
39
|
+
|
|
40
|
+
## Behavior
|
|
41
|
+
|
|
42
|
+
1. Checks command against dangerous patterns — blocks if matched
|
|
43
|
+
2. Checks command against safe allowlist — auto-approves if matched
|
|
44
|
+
3. Everything else: approves (passes through to Claude's normal permission flow)
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hasna/hook-permissionguard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code hook that auto-approves safe commands and blocks dangerous ones",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hook-permissionguard": "./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
|
+
"permission",
|
|
33
|
+
"guard",
|
|
34
|
+
"safety",
|
|
35
|
+
"allowlist",
|
|
36
|
+
"blocklist",
|
|
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,268 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Hook: permissionguard
|
|
5
|
+
*
|
|
6
|
+
* PreToolUse hook that auto-approves safe read-only commands and
|
|
7
|
+
* blocks dangerous patterns. Everything else passes through.
|
|
8
|
+
*
|
|
9
|
+
* Safe commands (auto-approve):
|
|
10
|
+
* - git status, git log, git diff, git branch
|
|
11
|
+
* - ls, cat, head, tail, wc, find, grep
|
|
12
|
+
* - npm test, bun test, pytest, cargo test
|
|
13
|
+
* - npm list, bun pm ls, pip list
|
|
14
|
+
* - node --version, bun --version, python --version
|
|
15
|
+
*
|
|
16
|
+
* Dangerous patterns (auto-block):
|
|
17
|
+
* - rm -rf / or ~ or $HOME
|
|
18
|
+
* - Fork bombs
|
|
19
|
+
* - dd if=, mkfs., fdisk
|
|
20
|
+
* - curl|sh, wget|sh (pipe to shell)
|
|
21
|
+
* - chmod 777
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync } from "fs";
|
|
25
|
+
|
|
26
|
+
interface HookInput {
|
|
27
|
+
session_id: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
tool_name: string;
|
|
30
|
+
tool_input: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface HookOutput {
|
|
34
|
+
decision: "approve" | "block";
|
|
35
|
+
reason?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readStdinJson(): HookInput | null {
|
|
39
|
+
try {
|
|
40
|
+
const input = readFileSync(0, "utf-8").trim();
|
|
41
|
+
if (!input) return null;
|
|
42
|
+
return JSON.parse(input);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function respond(output: HookOutput): void {
|
|
49
|
+
console.log(JSON.stringify(output));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Patterns for safe read-only commands that can be auto-approved.
|
|
54
|
+
* These match the beginning of a command (after trimming).
|
|
55
|
+
*/
|
|
56
|
+
const SAFE_COMMAND_PATTERNS: RegExp[] = [
|
|
57
|
+
// Git read-only
|
|
58
|
+
/^git\s+status(\s|$)/,
|
|
59
|
+
/^git\s+log(\s|$)/,
|
|
60
|
+
/^git\s+diff(\s|$)/,
|
|
61
|
+
/^git\s+branch(\s|$)/,
|
|
62
|
+
/^git\s+show(\s|$)/,
|
|
63
|
+
/^git\s+remote\s+-v(\s|$)/,
|
|
64
|
+
/^git\s+tag(\s|$)/,
|
|
65
|
+
|
|
66
|
+
// File reading
|
|
67
|
+
/^ls(\s|$)/,
|
|
68
|
+
/^cat\s/,
|
|
69
|
+
/^head\s/,
|
|
70
|
+
/^tail\s/,
|
|
71
|
+
/^wc\s/,
|
|
72
|
+
/^find\s/,
|
|
73
|
+
/^grep\s/,
|
|
74
|
+
/^rg\s/,
|
|
75
|
+
/^file\s/,
|
|
76
|
+
/^stat\s/,
|
|
77
|
+
/^du\s/,
|
|
78
|
+
/^df\s/,
|
|
79
|
+
/^which\s/,
|
|
80
|
+
/^type\s/,
|
|
81
|
+
/^pwd$/,
|
|
82
|
+
/^echo\s/,
|
|
83
|
+
|
|
84
|
+
// Testing
|
|
85
|
+
/^npm\s+test(\s|$)/,
|
|
86
|
+
/^npm\s+run\s+test(\s|$)/,
|
|
87
|
+
/^bun\s+test(\s|$)/,
|
|
88
|
+
/^bun\s+run\s+test(\s|$)/,
|
|
89
|
+
/^pytest(\s|$)/,
|
|
90
|
+
/^python\s+-m\s+pytest(\s|$)/,
|
|
91
|
+
/^cargo\s+test(\s|$)/,
|
|
92
|
+
/^go\s+test(\s|$)/,
|
|
93
|
+
/^jest(\s|$)/,
|
|
94
|
+
/^vitest(\s|$)/,
|
|
95
|
+
|
|
96
|
+
// Package listing
|
|
97
|
+
/^npm\s+list(\s|$)/,
|
|
98
|
+
/^npm\s+ls(\s|$)/,
|
|
99
|
+
/^bun\s+pm\s+ls(\s|$)/,
|
|
100
|
+
/^pip\s+list(\s|$)/,
|
|
101
|
+
/^pip\s+show\s/,
|
|
102
|
+
/^cargo\s+tree(\s|$)/,
|
|
103
|
+
|
|
104
|
+
// Version checks
|
|
105
|
+
/^node\s+--version$/,
|
|
106
|
+
/^node\s+-v$/,
|
|
107
|
+
/^bun\s+--version$/,
|
|
108
|
+
/^bun\s+-v$/,
|
|
109
|
+
/^python\s+--version$/,
|
|
110
|
+
/^python3\s+--version$/,
|
|
111
|
+
/^pip\s+--version$/,
|
|
112
|
+
/^cargo\s+--version$/,
|
|
113
|
+
/^go\s+version$/,
|
|
114
|
+
/^rustc\s+--version$/,
|
|
115
|
+
/^ruby\s+--version$/,
|
|
116
|
+
/^java\s+--version$/,
|
|
117
|
+
/^java\s+-version$/,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Dangerous patterns that should always be blocked.
|
|
122
|
+
*/
|
|
123
|
+
const DANGEROUS_PATTERNS: Array<{ pattern: RegExp; description: string }> = [
|
|
124
|
+
// Destructive rm commands
|
|
125
|
+
{
|
|
126
|
+
pattern: /rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive\s+--force|-[a-zA-Z]*f[a-zA-Z]*r)\s+[/~]/,
|
|
127
|
+
description: "rm -rf on root or home directory",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
pattern: /rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive\s+--force|-[a-zA-Z]*f[a-zA-Z]*r)\s+\$HOME/,
|
|
131
|
+
description: "rm -rf $HOME",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
pattern: /rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|--recursive\s+--force|-[a-zA-Z]*f[a-zA-Z]*r)\s+\/\s*$/,
|
|
135
|
+
description: "rm -rf /",
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// Fork bomb
|
|
139
|
+
{
|
|
140
|
+
pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
|
|
141
|
+
description: "fork bomb",
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// Disk destruction
|
|
145
|
+
{
|
|
146
|
+
pattern: /\bdd\s+if=/,
|
|
147
|
+
description: "dd command (raw disk write)",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
pattern: /\bmkfs\./,
|
|
151
|
+
description: "mkfs (filesystem format)",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
pattern: /\bfdisk\b/,
|
|
155
|
+
description: "fdisk (partition manipulation)",
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// Pipe to shell (remote code execution)
|
|
159
|
+
{
|
|
160
|
+
pattern: /curl\s+.*\|\s*(ba)?sh/,
|
|
161
|
+
description: "curl piped to shell (remote code execution)",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
pattern: /wget\s+.*\|\s*(ba)?sh/,
|
|
165
|
+
description: "wget piped to shell (remote code execution)",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
pattern: /curl\s+.*\|\s*sudo\s+(ba)?sh/,
|
|
169
|
+
description: "curl piped to sudo shell (remote code execution)",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
pattern: /wget\s+.*\|\s*sudo\s+(ba)?sh/,
|
|
173
|
+
description: "wget piped to sudo shell (remote code execution)",
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
// Insecure permissions
|
|
177
|
+
{
|
|
178
|
+
pattern: /chmod\s+(-R\s+)?777\b/,
|
|
179
|
+
description: "chmod 777 (world-writable permissions)",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
pattern: /chmod\s+-R\s+777\b/,
|
|
183
|
+
description: "chmod -R 777 (recursive world-writable permissions)",
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Additional dangerous patterns
|
|
187
|
+
{
|
|
188
|
+
pattern: /\bshutdown\b/,
|
|
189
|
+
description: "system shutdown",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
pattern: /\breboot\b/,
|
|
193
|
+
description: "system reboot",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
pattern: />\s*\/dev\/sda/,
|
|
197
|
+
description: "writing to raw disk device",
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
function isSafeCommand(command: string): boolean {
|
|
202
|
+
const trimmed = command.trim();
|
|
203
|
+
|
|
204
|
+
// Check each line of a multi-line or piped command
|
|
205
|
+
// If the FIRST command in a pipeline is safe and there are no pipes, approve
|
|
206
|
+
// For piped commands, don't auto-approve (could pipe safe command to dangerous one)
|
|
207
|
+
if (trimmed.includes("|") || trimmed.includes("&&") || trimmed.includes(";")) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const pattern of SAFE_COMMAND_PATTERNS) {
|
|
212
|
+
if (pattern.test(trimmed)) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isDangerousCommand(command: string): { dangerous: boolean; reason?: string } {
|
|
221
|
+
for (const { pattern, description } of DANGEROUS_PATTERNS) {
|
|
222
|
+
if (pattern.test(command)) {
|
|
223
|
+
return { dangerous: true, reason: `Blocked: ${description}` };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { dangerous: false };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function run(): void {
|
|
230
|
+
const input = readStdinJson();
|
|
231
|
+
|
|
232
|
+
if (!input) {
|
|
233
|
+
respond({ decision: "approve" });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (input.tool_name !== "Bash") {
|
|
238
|
+
respond({ decision: "approve" });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const command = input.tool_input?.command as string;
|
|
243
|
+
if (!command || typeof command !== "string") {
|
|
244
|
+
respond({ decision: "approve" });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check for dangerous patterns first (highest priority)
|
|
249
|
+
const dangerCheck = isDangerousCommand(command);
|
|
250
|
+
if (dangerCheck.dangerous) {
|
|
251
|
+
console.error(`[hook-permissionguard] ${dangerCheck.reason}`);
|
|
252
|
+
respond({ decision: "block", reason: dangerCheck.reason });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check for safe commands (auto-approve without prompting)
|
|
257
|
+
if (isSafeCommand(command)) {
|
|
258
|
+
respond({ decision: "approve" });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Everything else: approve (pass through to Claude's normal permission flow)
|
|
263
|
+
respond({ decision: "approve" });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (import.meta.main) {
|
|
267
|
+
run();
|
|
268
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# hook-promptguard
|
|
2
|
+
|
|
3
|
+
Claude Code hook that blocks prompt injection, credential extraction, and social engineering attempts.
|
|
4
|
+
|
|
5
|
+
## Event
|
|
6
|
+
|
|
7
|
+
**UserPromptSubmit** — fires before Claude processes a user prompt.
|
|
8
|
+
|
|
9
|
+
## What It Does
|
|
10
|
+
|
|
11
|
+
Scans user prompts for malicious patterns and blocks them before Claude sees them:
|
|
12
|
+
|
|
13
|
+
### Prompt Injection
|
|
14
|
+
- "ignore previous instructions", "disregard prior instructions"
|
|
15
|
+
- "new system prompt", "reveal system prompt", "what are your instructions"
|
|
16
|
+
- "you are now", "from now on you are", "entering new mode"
|
|
17
|
+
- "jailbreak", "DAN mode"
|
|
18
|
+
|
|
19
|
+
### Credential Access
|
|
20
|
+
- "show me the api key", "print the token", "reveal password"
|
|
21
|
+
- "dump credentials", "dump secrets", "extract credentials"
|
|
22
|
+
- "read .env", "cat .secrets/"
|
|
23
|
+
|
|
24
|
+
### Social Engineering
|
|
25
|
+
- "pretend you are", "act as root", "act as admin"
|
|
26
|
+
- "sudo mode", "god mode", "developer mode", "unrestricted mode"
|
|
27
|
+
- "bypass restrictions", "disable safety", "remove restrictions"
|
|
28
|
+
|
|
29
|
+
All matching is **case-insensitive**.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Add to your `.claude/settings.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"hooks": {
|
|
38
|
+
"UserPromptSubmit": [
|
|
39
|
+
{
|
|
40
|
+
"matcher": "",
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "bun run hooks/hook-promptguard/src/hook.ts"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Output
|
|
54
|
+
|
|
55
|
+
- `{ "decision": "approve" }` — prompt is safe
|
|
56
|
+
- `{ "decision": "block", "reason": "Blocked: potential prompt injection" }` — prompt blocked
|
|
57
|
+
|
|
58
|
+
## Customization
|
|
59
|
+
|
|
60
|
+
Add or remove patterns in the `INJECTION_PATTERNS`, `CREDENTIAL_PATTERNS`, or `SOCIAL_ENGINEERING_PATTERNS` arrays in `src/hook.ts`.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hook-promptguard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Claude Code hook that blocks prompt injection and social engineering attempts",
|
|
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,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
|
+
}
|