@malcomsonbrothers/claude-code-permission-hook 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/dist/logger.js ADDED
@@ -0,0 +1,45 @@
1
+ import { appendFileSync, existsSync, statSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { getConfigDir, loadConfig, ensureConfigDir } from "./config.js";
4
+ const LOG_FILE = "approval.jsonl";
5
+ function getLogPath() {
6
+ return join(getConfigDir(), LOG_FILE);
7
+ }
8
+ export function logDecision(entry) {
9
+ const config = loadConfig();
10
+ if (!config.logging.enabled) {
11
+ return;
12
+ }
13
+ ensureConfigDir();
14
+ const fullEntry = {
15
+ ...entry,
16
+ timestamp: new Date().toISOString(),
17
+ };
18
+ const line = JSON.stringify(fullEntry) + "\n";
19
+ appendFileSync(getLogPath(), line);
20
+ }
21
+ export function getLogPath_() {
22
+ return getLogPath();
23
+ }
24
+ export function logExists() {
25
+ return existsSync(getLogPath());
26
+ }
27
+ export function getLogStats() {
28
+ const logPath = getLogPath();
29
+ if (!existsSync(logPath)) {
30
+ return null;
31
+ }
32
+ try {
33
+ const stat = statSync(logPath);
34
+ const content = readFileSync(logPath, "utf-8");
35
+ const entries = content.trim().split("\n").filter(Boolean).length;
36
+ return {
37
+ entries,
38
+ sizeBytes: stat.size,
39
+ };
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGxE,MAAM,QAAQ,GAAG,gBAAgB,CAAC;AAElC,SAAS,UAAU;IACjB,OAAO,IAAI,CAAC,YAAY,EAAE,EAAE,QAAQ,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAkC;IAC5D,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QAC5B,OAAO;IACT,CAAC;IAED,eAAe,EAAE,CAAC;IAElB,MAAM,SAAS,GAAG;QAChB,GAAG,KAAK;QACR,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IAEF,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IAC9C,cAAc,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,UAAU,EAAE,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC/B,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;QAElE,OAAO;YACL,OAAO;YACP,SAAS,EAAE,IAAI,CAAC,IAAI;SACrB,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { PermissionRequestOutput } from "./types.js";
2
+ /**
3
+ * Handle a permission request from Claude Code.
4
+ * Returns PermissionRequestOutput for allow/deny, or null for passthrough.
5
+ * Passthrough means: exit 0 with no output, letting Claude show its native dialog.
6
+ */
7
+ export declare function handlePermissionRequest(rawInput: unknown): Promise<PermissionRequestOutput | null>;
8
+ //# sourceMappingURL=permission-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-handler.d.ts","sourceRoot":"","sources":["../src/permission-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,uBAAuB,EACxB,MAAM,YAAY,CAAC;AAOpB;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CA2GzC"}
@@ -0,0 +1,119 @@
1
+ import { PermissionRequestInputSchema, } from "./types.js";
2
+ import { checkFastDecision } from "./fast-decisions.js";
3
+ import { getCachedDecision, setCachedDecision } from "./cache.js";
4
+ import { queryLLM } from "./llm-client.js";
5
+ import { logDecision } from "./logger.js";
6
+ import { resolveProjectRoot } from "./project.js";
7
+ /**
8
+ * Handle a permission request from Claude Code.
9
+ * Returns PermissionRequestOutput for allow/deny, or null for passthrough.
10
+ * Passthrough means: exit 0 with no output, letting Claude show its native dialog.
11
+ */
12
+ export async function handlePermissionRequest(rawInput) {
13
+ // Parse and validate input
14
+ let input;
15
+ try {
16
+ input = PermissionRequestInputSchema.parse(rawInput);
17
+ }
18
+ catch (error) {
19
+ // Invalid input, deny
20
+ return createDenyResponse("Invalid permission request input");
21
+ }
22
+ const { tool_name: toolName, tool_input: toolInput, cwd, session_id: sessionId, } = input;
23
+ // Resolve the project root from cwd (.git > .claude > cwd fallback)
24
+ const projectRoot = cwd ? resolveProjectRoot(cwd) : undefined;
25
+ // Tier 1: Check fast decisions (hardcoded patterns)
26
+ const fastResult = checkFastDecision(toolName, toolInput);
27
+ if (fastResult.decision === "allow") {
28
+ logDecision({
29
+ toolName,
30
+ decision: "allow",
31
+ reason: fastResult.reason || "Fast allow",
32
+ decisionSource: "fast",
33
+ sessionId,
34
+ projectRoot,
35
+ });
36
+ return createAllowResponse();
37
+ }
38
+ if (fastResult.decision === "deny") {
39
+ logDecision({
40
+ toolName,
41
+ decision: "deny",
42
+ reason: fastResult.reason || "Fast deny",
43
+ decisionSource: "fast",
44
+ sessionId,
45
+ projectRoot,
46
+ });
47
+ return createDenyResponse(fastResult.reason || "Blocked by security pattern");
48
+ }
49
+ // Handle fast passthrough (e.g., AskUserQuestion - user must see and respond)
50
+ if (fastResult.decision === "passthrough") {
51
+ logDecision({
52
+ toolName,
53
+ decision: "passthrough",
54
+ reason: fastResult.reason || "Fast passthrough",
55
+ decisionSource: "fast",
56
+ sessionId,
57
+ projectRoot,
58
+ });
59
+ return null; // Signal passthrough - exit 0 with no output
60
+ }
61
+ // Tier 2: Check cache (note: passthrough decisions are never cached)
62
+ const cached = getCachedDecision(toolName, toolInput, projectRoot);
63
+ if (cached) {
64
+ logDecision({
65
+ toolName,
66
+ decision: cached.decision,
67
+ reason: `Cached: ${cached.reason}`,
68
+ decisionSource: "cache",
69
+ sessionId,
70
+ projectRoot,
71
+ });
72
+ if (cached.decision === "allow") {
73
+ return createAllowResponse();
74
+ }
75
+ else {
76
+ return createDenyResponse(cached.reason);
77
+ }
78
+ }
79
+ // Tier 3: Query LLM (returns allow/deny only - passthrough is handled by fast-decisions)
80
+ const llmResult = await queryLLM(toolName, toolInput, projectRoot);
81
+ // Cache the result
82
+ setCachedDecision(toolName, toolInput, llmResult.decision, llmResult.reason, projectRoot);
83
+ logDecision({
84
+ toolName,
85
+ decision: llmResult.decision,
86
+ reason: llmResult.reason,
87
+ decisionSource: "llm",
88
+ sessionId,
89
+ projectRoot,
90
+ });
91
+ if (llmResult.decision === "allow") {
92
+ return createAllowResponse();
93
+ }
94
+ else {
95
+ return createDenyResponse(llmResult.reason);
96
+ }
97
+ }
98
+ function createAllowResponse() {
99
+ return {
100
+ hookSpecificOutput: {
101
+ hookEventName: "PermissionRequest",
102
+ decision: {
103
+ behavior: "allow",
104
+ },
105
+ },
106
+ };
107
+ }
108
+ function createDenyResponse(message) {
109
+ return {
110
+ hookSpecificOutput: {
111
+ hookEventName: "PermissionRequest",
112
+ decision: {
113
+ behavior: "deny",
114
+ message,
115
+ },
116
+ },
117
+ };
118
+ }
119
+ //# sourceMappingURL=permission-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-handler.js","sourceRoot":"","sources":["../src/permission-handler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Resolve the project root by walking up from the given cwd.
3
+ * Looks for .git directory first, then .claude directory, falls back to cwd.
4
+ */
5
+ export declare function resolveProjectRoot(cwd: string): string;
6
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BtD"}
@@ -0,0 +1,35 @@
1
+ import { existsSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ /**
4
+ * Resolve the project root by walking up from the given cwd.
5
+ * Looks for .git directory first, then .claude directory, falls back to cwd.
6
+ */
7
+ export function resolveProjectRoot(cwd) {
8
+ let current = cwd;
9
+ // Walk up looking for .git
10
+ while (true) {
11
+ if (existsSync(join(current, ".git"))) {
12
+ return current;
13
+ }
14
+ const parent = dirname(current);
15
+ if (parent === current) {
16
+ break;
17
+ }
18
+ current = parent;
19
+ }
20
+ // Second pass: walk up looking for .claude directory
21
+ current = cwd;
22
+ while (true) {
23
+ if (existsSync(join(current, ".claude"))) {
24
+ return current;
25
+ }
26
+ const parent = dirname(current);
27
+ if (parent === current) {
28
+ break;
29
+ }
30
+ current = parent;
31
+ }
32
+ // Fallback: return cwd as-is
33
+ return cwd;
34
+ }
35
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAErC;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,IAAI,OAAO,GAAG,GAAG,CAAC;IAElB,2BAA2B;IAC3B,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;YACtC,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,MAAM;QACR,CAAC;QACD,OAAO,GAAG,MAAM,CAAC;IACnB,CAAC;IAED,qDAAqD;IACrD,OAAO,GAAG,GAAG,CAAC;IACd,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC;YACzC,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,MAAM;QACR,CAAC;QACD,OAAO,GAAG,MAAM,CAAC;IACnB,CAAC;IAED,6BAA6B;IAC7B,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,117 @@
1
+ import { z } from "zod";
2
+ export declare const PermissionRequestInputSchema: z.ZodObject<{
3
+ hook_event_name: z.ZodLiteral<"PermissionRequest">;
4
+ tool_name: z.ZodString;
5
+ tool_input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
6
+ transcript: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
7
+ session_id: z.ZodOptional<z.ZodString>;
8
+ cwd: z.ZodOptional<z.ZodString>;
9
+ }, z.core.$strip>;
10
+ export type PermissionRequestInput = z.infer<typeof PermissionRequestInputSchema>;
11
+ export declare const PermissionDecisionSchema: z.ZodObject<{
12
+ behavior: z.ZodEnum<{
13
+ allow: "allow";
14
+ deny: "deny";
15
+ }>;
16
+ updatedInput: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
17
+ message: z.ZodOptional<z.ZodString>;
18
+ }, z.core.$strip>;
19
+ export declare const PermissionRequestOutputSchema: z.ZodObject<{
20
+ hookSpecificOutput: z.ZodObject<{
21
+ hookEventName: z.ZodLiteral<"PermissionRequest">;
22
+ decision: z.ZodObject<{
23
+ behavior: z.ZodEnum<{
24
+ allow: "allow";
25
+ deny: "deny";
26
+ }>;
27
+ updatedInput: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
28
+ message: z.ZodOptional<z.ZodString>;
29
+ }, z.core.$strip>;
30
+ }, z.core.$strip>;
31
+ }, z.core.$strip>;
32
+ export type PermissionRequestOutput = z.infer<typeof PermissionRequestOutputSchema>;
33
+ export type PermissionDecision = z.infer<typeof PermissionDecisionSchema>;
34
+ export declare const DEFAULT_SYSTEM_PROMPT = "You are a security-focused AI assistant that evaluates Claude Code tool requests for auto-approval.\n\nYou will receive a tool name, the project root directory, and the tool input. Your job is to decide whether the request should be automatically approved or denied.\n\nCONTEXT:\n- \"Project Root\" is the root of the developer's project (where .git lives). Operations anywhere within the project root are standard development operations and are generally safe.\n- Subdirectories within the project root (e.g. monorepo packages) are still part of the project.\n\nALWAYS DENY:\n- Destructive system commands (rm -rf /, format drives, etc.)\n- Force pushing to protected branches: git push --force / git push -f to main, master, production, staging, develop\n- Commands that exfiltrate credentials or sensitive data to external services (e.g. curl posting /etc/passwd or env vars to a remote URL)\n- Fork bombs or resource exhaustion attacks\n- Any command that modifies system files (/etc, /usr, /bin, /sbin, /boot, Windows/System32, C:\\Windows)\n\nALWAYS ALLOW:\n- Reading files is low-risk regardless of path. Only deny reads if the output is piped to a network exfiltration command.\n- Standard development operations: npm/yarn/pnpm commands, git add, git commit, git push (without --force/-f), building, testing, linting\n- File creation, editing, and deletion within the project root\n- mkdir for paths inside or relative to the project root\n- Writing standard project files: .claude/*, config files, package.json, tsconfig.json, etc.\n- Test execution (npm test, vitest, jest, pytest, etc.)\n- Package installation (npm install, pip install, etc.)\n- Network requests to localhost or well-known APIs (github.com, npmjs.org, pypi.org, etc.)\n- git push (without --force or -f flags) to any branch\n\nNUANCED CASES:\n- git push --force or git push -f: DENY if targeting protected branches (main, master, production, staging, develop). ALLOW if targeting a feature/personal branch.\n- rm / del targeting specific files within the project: ALLOW. rm -rf of directories within the project: ALLOW with caution. rm -rf outside the project: DENY.\n- curl/wget: ALLOW if fetching data. DENY if posting sensitive data (env vars, credentials, private keys) to external URLs.\n- docker commands within the project: generally ALLOW.\n- Copying files from system paths (e.g. node_modules) into the project: ALLOW.\n\nDEFAULT TO ALLOW for standard development operations. Only DENY genuinely dangerous commands.\n\nRespond with JSON only:\n{\n \"decision\": \"allow\" | \"deny\",\n \"reason\": \"Brief explanation of your decision\"\n}";
35
+ export declare const ConfigSchema: z.ZodObject<{
36
+ llm: z.ZodPrefault<z.ZodObject<{
37
+ provider: z.ZodDefault<z.ZodEnum<{
38
+ openrouter: "openrouter";
39
+ openai: "openai";
40
+ anthropic: "anthropic";
41
+ }>>;
42
+ apiKey: z.ZodOptional<z.ZodString>;
43
+ model: z.ZodDefault<z.ZodString>;
44
+ baseUrl: z.ZodOptional<z.ZodString>;
45
+ systemPrompt: z.ZodDefault<z.ZodString>;
46
+ }, z.core.$strip>>;
47
+ cache: z.ZodPrefault<z.ZodObject<{
48
+ enabled: z.ZodDefault<z.ZodBoolean>;
49
+ ttlHours: z.ZodDefault<z.ZodNumber>;
50
+ }, z.core.$strip>>;
51
+ logging: z.ZodPrefault<z.ZodObject<{
52
+ enabled: z.ZodDefault<z.ZodBoolean>;
53
+ level: z.ZodDefault<z.ZodEnum<{
54
+ debug: "debug";
55
+ info: "info";
56
+ warn: "warn";
57
+ error: "error";
58
+ }>>;
59
+ }, z.core.$strip>>;
60
+ customAllowPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
61
+ customDenyPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
62
+ customPassthroughPatterns: z.ZodDefault<z.ZodArray<z.ZodString>>;
63
+ }, z.core.$strip>;
64
+ export type Config = z.infer<typeof ConfigSchema>;
65
+ export declare const CacheEntrySchema: z.ZodObject<{
66
+ key: z.ZodString;
67
+ decision: z.ZodEnum<{
68
+ allow: "allow";
69
+ deny: "deny";
70
+ }>;
71
+ reason: z.ZodString;
72
+ timestamp: z.ZodNumber;
73
+ toolName: z.ZodString;
74
+ toolInput: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
75
+ projectRoot: z.ZodOptional<z.ZodString>;
76
+ }, z.core.$strip>;
77
+ export type CacheEntry = z.infer<typeof CacheEntrySchema>;
78
+ export declare const CacheFileSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
79
+ key: z.ZodString;
80
+ decision: z.ZodEnum<{
81
+ allow: "allow";
82
+ deny: "deny";
83
+ }>;
84
+ reason: z.ZodString;
85
+ timestamp: z.ZodNumber;
86
+ toolName: z.ZodString;
87
+ toolInput: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
88
+ projectRoot: z.ZodOptional<z.ZodString>;
89
+ }, z.core.$strip>>;
90
+ export type CacheFile = z.infer<typeof CacheFileSchema>;
91
+ export declare const LogEntrySchema: z.ZodObject<{
92
+ timestamp: z.ZodString;
93
+ sessionId: z.ZodOptional<z.ZodString>;
94
+ toolName: z.ZodString;
95
+ decision: z.ZodEnum<{
96
+ allow: "allow";
97
+ deny: "deny";
98
+ passthrough: "passthrough";
99
+ }>;
100
+ reason: z.ZodString;
101
+ decisionSource: z.ZodEnum<{
102
+ llm: "llm";
103
+ cache: "cache";
104
+ fast: "fast";
105
+ }>;
106
+ projectRoot: z.ZodOptional<z.ZodString>;
107
+ }, z.core.$strip>;
108
+ export type LogEntry = z.infer<typeof LogEntrySchema>;
109
+ export declare const LLMResponseSchema: z.ZodObject<{
110
+ decision: z.ZodEnum<{
111
+ allow: "allow";
112
+ deny: "deny";
113
+ }>;
114
+ reason: z.ZodString;
115
+ }, z.core.$strip>;
116
+ export type LLMResponse = z.infer<typeof LLMResponseSchema>;
117
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,4BAA4B;;;;;;;iBAOvC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAC1C,OAAO,4BAA4B,CACpC,CAAC;AAGF,eAAO,MAAM,wBAAwB;;;;;;;iBAInC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;iBAKxC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AACF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAG1E,eAAO,MAAM,qBAAqB,0kFAuChC,CAAC;AAGH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2BvB,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAGlD,eAAO,MAAM,gBAAgB;;;;;;;;;;;iBAQ3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAG1D,eAAO,MAAM,eAAe;;;;;;;;;;;kBAAyC,CAAC;AAEtE,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAGxD,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;iBAQzB,CAAC;AAEH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAGtD,eAAO,MAAM,iBAAiB;;;;;;iBAG5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,120 @@
1
+ import { z } from "zod";
2
+ // PermissionRequest Input Schema
3
+ export const PermissionRequestInputSchema = z.object({
4
+ hook_event_name: z.literal("PermissionRequest"),
5
+ tool_name: z.string(),
6
+ tool_input: z.record(z.string(), z.unknown()),
7
+ transcript: z.array(z.unknown()).optional(),
8
+ session_id: z.string().optional(),
9
+ cwd: z.string().optional(),
10
+ });
11
+ // PermissionRequest Output Schema
12
+ export const PermissionDecisionSchema = z.object({
13
+ behavior: z.enum(["allow", "deny"]),
14
+ updatedInput: z.record(z.string(), z.unknown()).optional(),
15
+ message: z.string().optional(),
16
+ });
17
+ export const PermissionRequestOutputSchema = z.object({
18
+ hookSpecificOutput: z.object({
19
+ hookEventName: z.literal("PermissionRequest"),
20
+ decision: PermissionDecisionSchema,
21
+ }),
22
+ });
23
+ // Default LLM System Prompt - can be customized in config
24
+ export const DEFAULT_SYSTEM_PROMPT = `You are a security-focused AI assistant that evaluates Claude Code tool requests for auto-approval.
25
+
26
+ You will receive a tool name, the project root directory, and the tool input. Your job is to decide whether the request should be automatically approved or denied.
27
+
28
+ CONTEXT:
29
+ - "Project Root" is the root of the developer's project (where .git lives). Operations anywhere within the project root are standard development operations and are generally safe.
30
+ - Subdirectories within the project root (e.g. monorepo packages) are still part of the project.
31
+
32
+ ALWAYS DENY:
33
+ - Destructive system commands (rm -rf /, format drives, etc.)
34
+ - Force pushing to protected branches: git push --force / git push -f to main, master, production, staging, develop
35
+ - Commands that exfiltrate credentials or sensitive data to external services (e.g. curl posting /etc/passwd or env vars to a remote URL)
36
+ - Fork bombs or resource exhaustion attacks
37
+ - Any command that modifies system files (/etc, /usr, /bin, /sbin, /boot, Windows/System32, C:\\Windows)
38
+
39
+ ALWAYS ALLOW:
40
+ - Reading files is low-risk regardless of path. Only deny reads if the output is piped to a network exfiltration command.
41
+ - Standard development operations: npm/yarn/pnpm commands, git add, git commit, git push (without --force/-f), building, testing, linting
42
+ - File creation, editing, and deletion within the project root
43
+ - mkdir for paths inside or relative to the project root
44
+ - Writing standard project files: .claude/*, config files, package.json, tsconfig.json, etc.
45
+ - Test execution (npm test, vitest, jest, pytest, etc.)
46
+ - Package installation (npm install, pip install, etc.)
47
+ - Network requests to localhost or well-known APIs (github.com, npmjs.org, pypi.org, etc.)
48
+ - git push (without --force or -f flags) to any branch
49
+
50
+ NUANCED CASES:
51
+ - git push --force or git push -f: DENY if targeting protected branches (main, master, production, staging, develop). ALLOW if targeting a feature/personal branch.
52
+ - rm / del targeting specific files within the project: ALLOW. rm -rf of directories within the project: ALLOW with caution. rm -rf outside the project: DENY.
53
+ - curl/wget: ALLOW if fetching data. DENY if posting sensitive data (env vars, credentials, private keys) to external URLs.
54
+ - docker commands within the project: generally ALLOW.
55
+ - Copying files from system paths (e.g. node_modules) into the project: ALLOW.
56
+
57
+ DEFAULT TO ALLOW for standard development operations. Only DENY genuinely dangerous commands.
58
+
59
+ Respond with JSON only:
60
+ {
61
+ "decision": "allow" | "deny",
62
+ "reason": "Brief explanation of your decision"
63
+ }`;
64
+ // Config Schema
65
+ export const ConfigSchema = z.object({
66
+ llm: z
67
+ .object({
68
+ provider: z
69
+ .enum(["openrouter", "openai", "anthropic"])
70
+ .default("openrouter"),
71
+ apiKey: z.string().optional(),
72
+ model: z.string().default("gpt-4o-mini"),
73
+ baseUrl: z.string().optional(),
74
+ systemPrompt: z.string().default(DEFAULT_SYSTEM_PROMPT),
75
+ })
76
+ .prefault({}),
77
+ cache: z
78
+ .object({
79
+ enabled: z.boolean().default(true),
80
+ ttlHours: z.number().default(168), // 1 week
81
+ })
82
+ .prefault({}),
83
+ logging: z
84
+ .object({
85
+ enabled: z.boolean().default(true),
86
+ level: z.enum(["debug", "info", "warn", "error"]).default("info"),
87
+ })
88
+ .prefault({}),
89
+ customAllowPatterns: z.array(z.string()).default([]),
90
+ customDenyPatterns: z.array(z.string()).default([]),
91
+ customPassthroughPatterns: z.array(z.string()).default([]),
92
+ });
93
+ // Cache Entry Schema (passthrough decisions are not cached - they go to user each time)
94
+ export const CacheEntrySchema = z.object({
95
+ key: z.string(),
96
+ decision: z.enum(["allow", "deny"]),
97
+ reason: z.string(),
98
+ timestamp: z.number(),
99
+ toolName: z.string(),
100
+ toolInput: z.record(z.string(), z.unknown()).optional(),
101
+ projectRoot: z.string().optional(),
102
+ });
103
+ // Cache File Schema
104
+ export const CacheFileSchema = z.record(z.string(), CacheEntrySchema);
105
+ // Log Entry Schema
106
+ export const LogEntrySchema = z.object({
107
+ timestamp: z.string(),
108
+ sessionId: z.string().optional(),
109
+ toolName: z.string(),
110
+ decision: z.enum(["allow", "deny", "passthrough"]),
111
+ reason: z.string(),
112
+ decisionSource: z.enum(["fast", "cache", "llm"]),
113
+ projectRoot: z.string().optional(),
114
+ });
115
+ // LLM Response Schema (allow/deny only - passthrough is handled by fast-decisions)
116
+ export const LLMResponseSchema = z.object({
117
+ decision: z.enum(["allow", "deny"]),
118
+ reason: z.string(),
119
+ });
120
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@malcomsonbrothers/claude-code-permission-hook",
3
+ "version": "0.1.0",
4
+ "description": "Intelligent auto-approval hook for Claude Code using PermissionRequest",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "cc-approve": "./bin/cc-approve.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsc --watch",
14
+ "test": "vitest",
15
+ "lint": "eslint src/",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "claude",
20
+ "claude-code",
21
+ "hooks",
22
+ "auto-approve",
23
+ "permission",
24
+ "ai",
25
+ "automation"
26
+ ],
27
+ "author": "Malcomson Brothers",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/malcomsonbrothers/claude-code-permission-hook"
32
+ },
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "bin"
39
+ ],
40
+ "dependencies": {
41
+ "chalk": "^5.6.2",
42
+ "commander": "^14.0.2",
43
+ "inquirer": "^13.2.2",
44
+ "openai": "^6.17.0",
45
+ "pino": "^10.3.0",
46
+ "pino-pretty": "^13.1.3",
47
+ "zod": "^4.3.6"
48
+ },
49
+ "devDependencies": {
50
+ "@types/inquirer": "^9.0.9",
51
+ "@types/node": "^25.1.0",
52
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
53
+ "@typescript-eslint/parser": "^8.54.0",
54
+ "eslint": "^9.39.2",
55
+ "typescript": "^5.9.3",
56
+ "vitest": "^4.0.18"
57
+ }
58
+ }