@hasna/hooks 0.0.7 → 0.1.1
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 +240 -42
- package/dist/index.js +228 -30
- 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 +3 -3
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code Hook: errornotify
|
|
5
|
+
*
|
|
6
|
+
* PostToolUse hook that detects tool failures and logs errors.
|
|
7
|
+
* Checks tool output for error indicators (non-zero exit codes,
|
|
8
|
+
* error messages) and logs warnings to stderr. Optionally writes
|
|
9
|
+
* to a .claude/errors.log file for persistent error tracking.
|
|
10
|
+
*
|
|
11
|
+
* Never blocks — always outputs { continue: true }.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, appendFileSync, mkdirSync, existsSync } from "fs";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
|
|
17
|
+
interface HookInput {
|
|
18
|
+
session_id: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
tool_name: string;
|
|
21
|
+
tool_input: Record<string, unknown>;
|
|
22
|
+
tool_output?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface HookOutput {
|
|
26
|
+
continue: true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read and parse JSON from stdin
|
|
31
|
+
*/
|
|
32
|
+
function readStdinJson(): HookInput | null {
|
|
33
|
+
try {
|
|
34
|
+
const input = readFileSync(0, "utf-8").trim();
|
|
35
|
+
if (!input) return null;
|
|
36
|
+
return JSON.parse(input);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if the tool output indicates a failure
|
|
44
|
+
*/
|
|
45
|
+
function detectError(input: HookInput): { isError: boolean; message: string } {
|
|
46
|
+
const output = input.tool_output || {};
|
|
47
|
+
|
|
48
|
+
// Check for explicit exit code
|
|
49
|
+
const exitCode = output.exit_code ?? output.exitCode ?? output.code;
|
|
50
|
+
if (exitCode !== undefined && exitCode !== null && exitCode !== 0) {
|
|
51
|
+
const stderr = (output.stderr as string) || (output.output as string) || "unknown error";
|
|
52
|
+
return {
|
|
53
|
+
isError: true,
|
|
54
|
+
message: `Exit code ${exitCode}: ${truncate(stderr, 200)}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check for error field
|
|
59
|
+
if (output.error && typeof output.error === "string") {
|
|
60
|
+
return {
|
|
61
|
+
isError: true,
|
|
62
|
+
message: `Error: ${truncate(output.error, 200)}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check output text for common error indicators
|
|
67
|
+
const outputText =
|
|
68
|
+
(output.stderr as string) ||
|
|
69
|
+
(output.output as string) ||
|
|
70
|
+
(output.content as string) ||
|
|
71
|
+
(output.text as string) ||
|
|
72
|
+
"";
|
|
73
|
+
|
|
74
|
+
if (typeof outputText === "string" && outputText.length > 0) {
|
|
75
|
+
// Check for common error patterns in output
|
|
76
|
+
const errorPatterns = [
|
|
77
|
+
/^error:/im,
|
|
78
|
+
/^fatal:/im,
|
|
79
|
+
/^panic:/im,
|
|
80
|
+
/command not found/i,
|
|
81
|
+
/permission denied/i,
|
|
82
|
+
/no such file or directory/i,
|
|
83
|
+
/segmentation fault/i,
|
|
84
|
+
/killed/i,
|
|
85
|
+
/ENOENT/,
|
|
86
|
+
/EACCES/,
|
|
87
|
+
/EPERM/,
|
|
88
|
+
/ENOMEM/,
|
|
89
|
+
/TypeError:/,
|
|
90
|
+
/ReferenceError:/,
|
|
91
|
+
/SyntaxError:/,
|
|
92
|
+
/ModuleNotFoundError:/,
|
|
93
|
+
/ImportError:/,
|
|
94
|
+
/FileNotFoundError:/,
|
|
95
|
+
/PermissionError:/,
|
|
96
|
+
/traceback \(most recent call last\)/i,
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
for (const pattern of errorPatterns) {
|
|
100
|
+
if (pattern.test(outputText)) {
|
|
101
|
+
// Extract the first relevant line
|
|
102
|
+
const lines = outputText.split("\n").filter((l: string) => l.trim());
|
|
103
|
+
const errorLine = lines.find((l: string) => pattern.test(l)) || lines[0] || "";
|
|
104
|
+
return {
|
|
105
|
+
isError: true,
|
|
106
|
+
message: truncate(errorLine.trim(), 200),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { isError: false, message: "" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Truncate a string to a maximum length
|
|
117
|
+
*/
|
|
118
|
+
function truncate(str: string, maxLen: number): string {
|
|
119
|
+
if (str.length <= maxLen) return str;
|
|
120
|
+
return str.slice(0, maxLen) + "...";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get a human-readable description of what was being executed
|
|
125
|
+
*/
|
|
126
|
+
function getToolContext(input: HookInput): string {
|
|
127
|
+
const toolName = input.tool_name;
|
|
128
|
+
const toolInput = input.tool_input || {};
|
|
129
|
+
|
|
130
|
+
switch (toolName) {
|
|
131
|
+
case "Bash":
|
|
132
|
+
return `Bash: ${truncate((toolInput.command as string) || "unknown command", 100)}`;
|
|
133
|
+
case "Write":
|
|
134
|
+
case "Edit":
|
|
135
|
+
return `${toolName}: ${(toolInput.file_path as string) || "unknown file"}`;
|
|
136
|
+
case "Read":
|
|
137
|
+
return `Read: ${(toolInput.file_path as string) || "unknown file"}`;
|
|
138
|
+
default:
|
|
139
|
+
return toolName;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Write error to .claude/errors.log
|
|
145
|
+
*/
|
|
146
|
+
function writeErrorLog(cwd: string, toolContext: string, errorMessage: string, sessionId: string): void {
|
|
147
|
+
try {
|
|
148
|
+
const claudeDir = join(cwd, ".claude");
|
|
149
|
+
if (!existsSync(claudeDir)) {
|
|
150
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const logFile = join(claudeDir, "errors.log");
|
|
154
|
+
const timestamp = new Date().toISOString();
|
|
155
|
+
const entry = `[${timestamp}] [session:${sessionId.slice(0, 8)}] ${toolContext} — ${errorMessage}\n`;
|
|
156
|
+
appendFileSync(logFile, entry);
|
|
157
|
+
} catch {
|
|
158
|
+
// Silently fail — logging should never cause issues
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Output hook response
|
|
164
|
+
*/
|
|
165
|
+
function respond(): void {
|
|
166
|
+
const output: HookOutput = { continue: true };
|
|
167
|
+
console.log(JSON.stringify(output));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Main hook execution
|
|
172
|
+
*/
|
|
173
|
+
export function run(): void {
|
|
174
|
+
const input = readStdinJson();
|
|
175
|
+
|
|
176
|
+
if (!input) {
|
|
177
|
+
respond();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { isError, message } = detectError(input);
|
|
182
|
+
|
|
183
|
+
if (isError) {
|
|
184
|
+
const toolContext = getToolContext(input);
|
|
185
|
+
console.error(`[hook-errornotify] FAILURE in ${toolContext}`);
|
|
186
|
+
console.error(`[hook-errornotify] ${message}`);
|
|
187
|
+
|
|
188
|
+
// Write to persistent error log
|
|
189
|
+
writeErrorLog(input.cwd, toolContext, message, input.session_id);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
respond();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (import.meta.main) {
|
|
196
|
+
run();
|
|
197
|
+
}
|
|
@@ -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
|
+
}
|