sovr-mcp-proxy 6.0.1 → 7.0.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/LICENSE +54 -19
- package/README.md +387 -164
- package/dist/cli.js +1553 -24
- package/dist/cli.mjs +185 -18
- package/dist/commandNormalizer.d.mts +95 -0
- package/dist/commandNormalizer.d.ts +95 -0
- package/dist/commandNormalizer.js +365 -0
- package/dist/commandNormalizer.mjs +336 -0
- package/dist/hooksAdapter.d.mts +122 -0
- package/dist/hooksAdapter.d.ts +122 -0
- package/dist/hooksAdapter.js +321 -0
- package/dist/hooksAdapter.mjs +291 -0
- package/dist/index.js +1065 -2
- package/dist/index.mjs +98 -2
- package/dist/toolReplacement.d.mts +108 -0
- package/dist/toolReplacement.d.ts +108 -0
- package/dist/toolReplacement.js +234 -0
- package/dist/toolReplacement.mjs +204 -0
- package/dist/whitelistEngine.d.mts +167 -0
- package/dist/whitelistEngine.d.ts +167 -0
- package/dist/whitelistEngine.js +435 -0
- package/dist/whitelistEngine.mjs +403 -0
- package/package.json +46 -41
- package/server.json +0 -14
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sovr/proxy-mcp — Claude Code Hooks Adapter
|
|
3
|
+
*
|
|
4
|
+
* Solves the bypass problem for Claude Code users specifically.
|
|
5
|
+
* Claude Code supports "Hooks" — scripts that run before/after tool use.
|
|
6
|
+
* PreToolUse hooks can BLOCK tool calls before they execute.
|
|
7
|
+
*
|
|
8
|
+
* This adapter generates the correct hooks configuration for Claude Code
|
|
9
|
+
* that routes ALL tool calls through SOVR for policy evaluation.
|
|
10
|
+
*
|
|
11
|
+
* Unlike the MCP proxy approach, hooks are ENFORCED by Claude Code itself —
|
|
12
|
+
* the LLM cannot bypass them because they run at the platform level.
|
|
13
|
+
*
|
|
14
|
+
* Integration:
|
|
15
|
+
* npx sovr-mcp-proxy init --claude-code
|
|
16
|
+
* → Writes hooks config to ~/.claude/settings.json
|
|
17
|
+
* → Installs sovr-hook.sh as the PreToolUse handler
|
|
18
|
+
*
|
|
19
|
+
* Hook flow:
|
|
20
|
+
* 1. LLM decides to call a tool (e.g., Bash with "rm -rf /")
|
|
21
|
+
* 2. Claude Code triggers PreToolUse hook BEFORE execution
|
|
22
|
+
* 3. sovr-hook.sh calls SOVR daemon/API for policy evaluation
|
|
23
|
+
* 4. Hook exits 0 (allow) or 2 (block) based on SOVR decision
|
|
24
|
+
* 5. Claude Code either proceeds or blocks the tool call
|
|
25
|
+
*
|
|
26
|
+
* Exit codes (Claude Code hook spec):
|
|
27
|
+
* 0 = Allow (proceed with tool call)
|
|
28
|
+
* 2 = Block (reject tool call, show reason to user)
|
|
29
|
+
* Other = Error (Claude Code decides based on its own policy)
|
|
30
|
+
*
|
|
31
|
+
* @see https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
32
|
+
*/
|
|
33
|
+
/** Claude Code hook event types */
|
|
34
|
+
type HookEvent = 'PreToolUse' | 'PostToolUse' | 'Notification' | 'Stop' | 'SubagentStop';
|
|
35
|
+
/** A single hook matcher */
|
|
36
|
+
interface HookMatcher {
|
|
37
|
+
/** Tool name pattern (regex supported) */
|
|
38
|
+
tool_name?: string;
|
|
39
|
+
/** Notification type pattern */
|
|
40
|
+
type?: string;
|
|
41
|
+
}
|
|
42
|
+
/** A single hook definition */
|
|
43
|
+
interface HookDef {
|
|
44
|
+
/** Matchers that determine when this hook fires */
|
|
45
|
+
matcher: HookMatcher;
|
|
46
|
+
/** Array of hook commands to execute */
|
|
47
|
+
hooks: Array<{
|
|
48
|
+
/** The command to run */
|
|
49
|
+
command: string;
|
|
50
|
+
/** Timeout in seconds */
|
|
51
|
+
timeout?: number;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
/** Claude Code settings.json structure (partial) */
|
|
55
|
+
interface ClaudeCodeSettings {
|
|
56
|
+
hooks?: Record<HookEvent, HookDef[]>;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
/** Configuration for the hooks adapter */
|
|
60
|
+
interface HooksAdapterConfig {
|
|
61
|
+
/** SOVR API key */
|
|
62
|
+
apiKey?: string;
|
|
63
|
+
/** SOVR endpoint (default: daemon socket) */
|
|
64
|
+
endpoint?: string;
|
|
65
|
+
/** Fail mode: 'open' allows on error, 'closed' blocks on error */
|
|
66
|
+
failMode?: 'open' | 'closed';
|
|
67
|
+
/** Tools to intercept (regex patterns) */
|
|
68
|
+
toolPatterns?: string[];
|
|
69
|
+
/** Timeout for hook execution in seconds */
|
|
70
|
+
hookTimeout?: number;
|
|
71
|
+
/** Path to Claude Code settings (auto-detected if not provided) */
|
|
72
|
+
settingsPath?: string;
|
|
73
|
+
/** Whether to also add PostToolUse audit hook */
|
|
74
|
+
addAuditHook?: boolean;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Generate the sovr-hook.sh script content.
|
|
78
|
+
* This script is called by Claude Code for every PreToolUse event.
|
|
79
|
+
*
|
|
80
|
+
* It reads the hook input from stdin (JSON with tool_name, tool_input),
|
|
81
|
+
* calls SOVR for evaluation, and exits with the appropriate code.
|
|
82
|
+
*/
|
|
83
|
+
declare function generateHookScript(config: HooksAdapterConfig): string;
|
|
84
|
+
/**
|
|
85
|
+
* Generate the PostToolUse audit hook script.
|
|
86
|
+
* This records what happened after a tool call completes.
|
|
87
|
+
*/
|
|
88
|
+
declare function generateAuditHookScript(config: HooksAdapterConfig): string;
|
|
89
|
+
/**
|
|
90
|
+
* Generate the Claude Code hooks configuration object.
|
|
91
|
+
*/
|
|
92
|
+
declare function generateHooksConfig(config: HooksAdapterConfig): Record<HookEvent, HookDef[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Install SOVR hooks into Claude Code settings.
|
|
95
|
+
* Merges with existing settings, preserving non-SOVR hooks.
|
|
96
|
+
*/
|
|
97
|
+
declare function installHooks(config: HooksAdapterConfig): {
|
|
98
|
+
settingsPath: string;
|
|
99
|
+
hookScriptPath: string;
|
|
100
|
+
auditScriptPath?: string;
|
|
101
|
+
installed: boolean;
|
|
102
|
+
backup?: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Remove SOVR hooks from Claude Code settings.
|
|
106
|
+
*/
|
|
107
|
+
declare function uninstallHooks(config?: {
|
|
108
|
+
settingsPath?: string;
|
|
109
|
+
}): {
|
|
110
|
+
removed: boolean;
|
|
111
|
+
settingsPath: string;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Detect installed Claude Code and return its settings path.
|
|
115
|
+
*/
|
|
116
|
+
declare function detectClaudeCode(): {
|
|
117
|
+
found: boolean;
|
|
118
|
+
settingsPath?: string;
|
|
119
|
+
version?: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export { type ClaudeCodeSettings, type HookDef, type HookEvent, type HookMatcher, type HooksAdapterConfig, detectClaudeCode, generateAuditHookScript, generateHookScript, generateHooksConfig, installHooks, uninstallHooks };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sovr/proxy-mcp — Claude Code Hooks Adapter
|
|
3
|
+
*
|
|
4
|
+
* Solves the bypass problem for Claude Code users specifically.
|
|
5
|
+
* Claude Code supports "Hooks" — scripts that run before/after tool use.
|
|
6
|
+
* PreToolUse hooks can BLOCK tool calls before they execute.
|
|
7
|
+
*
|
|
8
|
+
* This adapter generates the correct hooks configuration for Claude Code
|
|
9
|
+
* that routes ALL tool calls through SOVR for policy evaluation.
|
|
10
|
+
*
|
|
11
|
+
* Unlike the MCP proxy approach, hooks are ENFORCED by Claude Code itself —
|
|
12
|
+
* the LLM cannot bypass them because they run at the platform level.
|
|
13
|
+
*
|
|
14
|
+
* Integration:
|
|
15
|
+
* npx sovr-mcp-proxy init --claude-code
|
|
16
|
+
* → Writes hooks config to ~/.claude/settings.json
|
|
17
|
+
* → Installs sovr-hook.sh as the PreToolUse handler
|
|
18
|
+
*
|
|
19
|
+
* Hook flow:
|
|
20
|
+
* 1. LLM decides to call a tool (e.g., Bash with "rm -rf /")
|
|
21
|
+
* 2. Claude Code triggers PreToolUse hook BEFORE execution
|
|
22
|
+
* 3. sovr-hook.sh calls SOVR daemon/API for policy evaluation
|
|
23
|
+
* 4. Hook exits 0 (allow) or 2 (block) based on SOVR decision
|
|
24
|
+
* 5. Claude Code either proceeds or blocks the tool call
|
|
25
|
+
*
|
|
26
|
+
* Exit codes (Claude Code hook spec):
|
|
27
|
+
* 0 = Allow (proceed with tool call)
|
|
28
|
+
* 2 = Block (reject tool call, show reason to user)
|
|
29
|
+
* Other = Error (Claude Code decides based on its own policy)
|
|
30
|
+
*
|
|
31
|
+
* @see https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
32
|
+
*/
|
|
33
|
+
/** Claude Code hook event types */
|
|
34
|
+
type HookEvent = 'PreToolUse' | 'PostToolUse' | 'Notification' | 'Stop' | 'SubagentStop';
|
|
35
|
+
/** A single hook matcher */
|
|
36
|
+
interface HookMatcher {
|
|
37
|
+
/** Tool name pattern (regex supported) */
|
|
38
|
+
tool_name?: string;
|
|
39
|
+
/** Notification type pattern */
|
|
40
|
+
type?: string;
|
|
41
|
+
}
|
|
42
|
+
/** A single hook definition */
|
|
43
|
+
interface HookDef {
|
|
44
|
+
/** Matchers that determine when this hook fires */
|
|
45
|
+
matcher: HookMatcher;
|
|
46
|
+
/** Array of hook commands to execute */
|
|
47
|
+
hooks: Array<{
|
|
48
|
+
/** The command to run */
|
|
49
|
+
command: string;
|
|
50
|
+
/** Timeout in seconds */
|
|
51
|
+
timeout?: number;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
/** Claude Code settings.json structure (partial) */
|
|
55
|
+
interface ClaudeCodeSettings {
|
|
56
|
+
hooks?: Record<HookEvent, HookDef[]>;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
/** Configuration for the hooks adapter */
|
|
60
|
+
interface HooksAdapterConfig {
|
|
61
|
+
/** SOVR API key */
|
|
62
|
+
apiKey?: string;
|
|
63
|
+
/** SOVR endpoint (default: daemon socket) */
|
|
64
|
+
endpoint?: string;
|
|
65
|
+
/** Fail mode: 'open' allows on error, 'closed' blocks on error */
|
|
66
|
+
failMode?: 'open' | 'closed';
|
|
67
|
+
/** Tools to intercept (regex patterns) */
|
|
68
|
+
toolPatterns?: string[];
|
|
69
|
+
/** Timeout for hook execution in seconds */
|
|
70
|
+
hookTimeout?: number;
|
|
71
|
+
/** Path to Claude Code settings (auto-detected if not provided) */
|
|
72
|
+
settingsPath?: string;
|
|
73
|
+
/** Whether to also add PostToolUse audit hook */
|
|
74
|
+
addAuditHook?: boolean;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Generate the sovr-hook.sh script content.
|
|
78
|
+
* This script is called by Claude Code for every PreToolUse event.
|
|
79
|
+
*
|
|
80
|
+
* It reads the hook input from stdin (JSON with tool_name, tool_input),
|
|
81
|
+
* calls SOVR for evaluation, and exits with the appropriate code.
|
|
82
|
+
*/
|
|
83
|
+
declare function generateHookScript(config: HooksAdapterConfig): string;
|
|
84
|
+
/**
|
|
85
|
+
* Generate the PostToolUse audit hook script.
|
|
86
|
+
* This records what happened after a tool call completes.
|
|
87
|
+
*/
|
|
88
|
+
declare function generateAuditHookScript(config: HooksAdapterConfig): string;
|
|
89
|
+
/**
|
|
90
|
+
* Generate the Claude Code hooks configuration object.
|
|
91
|
+
*/
|
|
92
|
+
declare function generateHooksConfig(config: HooksAdapterConfig): Record<HookEvent, HookDef[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Install SOVR hooks into Claude Code settings.
|
|
95
|
+
* Merges with existing settings, preserving non-SOVR hooks.
|
|
96
|
+
*/
|
|
97
|
+
declare function installHooks(config: HooksAdapterConfig): {
|
|
98
|
+
settingsPath: string;
|
|
99
|
+
hookScriptPath: string;
|
|
100
|
+
auditScriptPath?: string;
|
|
101
|
+
installed: boolean;
|
|
102
|
+
backup?: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Remove SOVR hooks from Claude Code settings.
|
|
106
|
+
*/
|
|
107
|
+
declare function uninstallHooks(config?: {
|
|
108
|
+
settingsPath?: string;
|
|
109
|
+
}): {
|
|
110
|
+
removed: boolean;
|
|
111
|
+
settingsPath: string;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Detect installed Claude Code and return its settings path.
|
|
115
|
+
*/
|
|
116
|
+
declare function detectClaudeCode(): {
|
|
117
|
+
found: boolean;
|
|
118
|
+
settingsPath?: string;
|
|
119
|
+
version?: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export { type ClaudeCodeSettings, type HookDef, type HookEvent, type HookMatcher, type HooksAdapterConfig, detectClaudeCode, generateAuditHookScript, generateHookScript, generateHooksConfig, installHooks, uninstallHooks };
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/hooksAdapter.ts
|
|
21
|
+
var hooksAdapter_exports = {};
|
|
22
|
+
__export(hooksAdapter_exports, {
|
|
23
|
+
detectClaudeCode: () => detectClaudeCode,
|
|
24
|
+
generateAuditHookScript: () => generateAuditHookScript,
|
|
25
|
+
generateHookScript: () => generateHookScript,
|
|
26
|
+
generateHooksConfig: () => generateHooksConfig,
|
|
27
|
+
installHooks: () => installHooks,
|
|
28
|
+
uninstallHooks: () => uninstallHooks
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(hooksAdapter_exports);
|
|
31
|
+
var import_node_fs = require("fs");
|
|
32
|
+
var import_node_path = require("path");
|
|
33
|
+
var import_node_os = require("os");
|
|
34
|
+
var DEFAULT_TOOL_PATTERNS = [
|
|
35
|
+
"Bash",
|
|
36
|
+
"bash",
|
|
37
|
+
"shell",
|
|
38
|
+
"execute_command",
|
|
39
|
+
"Write",
|
|
40
|
+
// File write operations
|
|
41
|
+
"Edit",
|
|
42
|
+
// File edit operations
|
|
43
|
+
"MultiEdit"
|
|
44
|
+
// Multi-file edit operations
|
|
45
|
+
];
|
|
46
|
+
var DEFAULT_HOOK_TIMEOUT = 10;
|
|
47
|
+
function generateHookScript(config) {
|
|
48
|
+
const failExit = config.failMode === "closed" ? 2 : 0;
|
|
49
|
+
const endpoint = config.endpoint || "http://localhost:45557";
|
|
50
|
+
const apiKeyRef = config.apiKey ? `"${config.apiKey}"` : '"${SOVR_API_KEY:-}"';
|
|
51
|
+
return `#!/bin/bash
|
|
52
|
+
# SOVR PreToolUse Hook for Claude Code
|
|
53
|
+
# Generated by: npx sovr-mcp-proxy init --claude-code
|
|
54
|
+
#
|
|
55
|
+
# This hook intercepts ALL tool calls and evaluates them against
|
|
56
|
+
# SOVR security policies BEFORE Claude Code executes them.
|
|
57
|
+
#
|
|
58
|
+
# Exit codes:
|
|
59
|
+
# 0 = ALLOW (proceed with tool call)
|
|
60
|
+
# 2 = BLOCK (reject tool call)
|
|
61
|
+
#
|
|
62
|
+
# Environment:
|
|
63
|
+
# SOVR_API_KEY \u2014 API key for SOVR authentication
|
|
64
|
+
# SOVR_ENDPOINT \u2014 SOVR daemon endpoint (default: ${endpoint})
|
|
65
|
+
# SOVR_FAIL_MODE \u2014 'open' (default) or 'closed'
|
|
66
|
+
# SOVR_HOOK_DEBUG \u2014 Set to '1' for debug logging
|
|
67
|
+
|
|
68
|
+
set -euo pipefail
|
|
69
|
+
|
|
70
|
+
# Configuration
|
|
71
|
+
SOVR_ENDPOINT="\${SOVR_ENDPOINT:-${endpoint}}"
|
|
72
|
+
SOVR_API_KEY=${apiKeyRef}
|
|
73
|
+
SOVR_FAIL_MODE="\${SOVR_FAIL_MODE:-${config.failMode || "open"}}"
|
|
74
|
+
SOVR_HOOK_DEBUG="\${SOVR_HOOK_DEBUG:-0}"
|
|
75
|
+
FAIL_EXIT=${failExit}
|
|
76
|
+
|
|
77
|
+
# Debug logging
|
|
78
|
+
debug() {
|
|
79
|
+
if [ "$SOVR_HOOK_DEBUG" = "1" ]; then
|
|
80
|
+
echo "[SOVR-HOOK $(date -u +%H:%M:%S)] $*" >&2
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Read hook input from stdin
|
|
85
|
+
INPUT=$(cat)
|
|
86
|
+
debug "Input: $INPUT"
|
|
87
|
+
|
|
88
|
+
# Extract tool name and input from JSON
|
|
89
|
+
TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "unknown")
|
|
90
|
+
TOOL_INPUT=$(echo "$INPUT" | grep -o '"tool_input":{[^}]*}' | head -1 2>/dev/null || echo "{}")
|
|
91
|
+
|
|
92
|
+
debug "Tool: $TOOL_NAME"
|
|
93
|
+
|
|
94
|
+
# Skip non-dangerous tools (read-only operations)
|
|
95
|
+
case "$TOOL_NAME" in
|
|
96
|
+
Read|View|LS|Glob|Grep|TodoRead|WebFetch|WebSearch)
|
|
97
|
+
debug "SKIP: Read-only tool $TOOL_NAME"
|
|
98
|
+
exit 0
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
101
|
+
|
|
102
|
+
# Extract command for Bash tools
|
|
103
|
+
COMMAND=""
|
|
104
|
+
case "$TOOL_NAME" in
|
|
105
|
+
Bash|bash|shell|execute_command)
|
|
106
|
+
COMMAND=$(echo "$TOOL_INPUT" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
|
|
107
|
+
;;
|
|
108
|
+
Write|Edit|MultiEdit)
|
|
109
|
+
COMMAND=$(echo "$TOOL_INPUT" | grep -o '"file_path":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
|
|
110
|
+
;;
|
|
111
|
+
esac
|
|
112
|
+
|
|
113
|
+
debug "Command: $COMMAND"
|
|
114
|
+
|
|
115
|
+
# Call SOVR daemon for evaluation
|
|
116
|
+
RESPONSE=$(curl -s -X POST "$SOVR_ENDPOINT/api/hook/evaluate" \\
|
|
117
|
+
-H "Content-Type: application/json" \\
|
|
118
|
+
-H "X-SOVR-API-Key: $SOVR_API_KEY" \\
|
|
119
|
+
-d "{
|
|
120
|
+
\\"tool_name\\": \\"$TOOL_NAME\\",
|
|
121
|
+
\\"command\\": $(echo "$COMMAND" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' 2>/dev/null || echo '""'),
|
|
122
|
+
\\"channel\\": \\"hooks\\",
|
|
123
|
+
\\"context\\": {
|
|
124
|
+
\\"platform\\": \\"claude-code\\",
|
|
125
|
+
\\"hook_event\\": \\"PreToolUse\\"
|
|
126
|
+
}
|
|
127
|
+
}" \\
|
|
128
|
+
--connect-timeout 3 --max-time 5 2>/dev/null) || {
|
|
129
|
+
debug "SOVR daemon unreachable, fail-$SOVR_FAIL_MODE"
|
|
130
|
+
exit $FAIL_EXIT
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
debug "Response: $RESPONSE"
|
|
134
|
+
|
|
135
|
+
# Parse verdict
|
|
136
|
+
VERDICT=$(echo "$RESPONSE" | grep -o '"verdict":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "")
|
|
137
|
+
REASON=$(echo "$RESPONSE" | grep -o '"reason":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "Policy evaluation failed")
|
|
138
|
+
|
|
139
|
+
case "$VERDICT" in
|
|
140
|
+
allow)
|
|
141
|
+
debug "ALLOWED: $TOOL_NAME"
|
|
142
|
+
exit 0
|
|
143
|
+
;;
|
|
144
|
+
deny)
|
|
145
|
+
debug "BLOCKED: $TOOL_NAME \u2014 $REASON"
|
|
146
|
+
# Output block reason as JSON for Claude Code to display
|
|
147
|
+
echo "{\\"error\\": \\"SOVR Policy Violation: $REASON\\"}"
|
|
148
|
+
exit 2
|
|
149
|
+
;;
|
|
150
|
+
escalate)
|
|
151
|
+
debug "ESCALATED: $TOOL_NAME \u2014 requires approval"
|
|
152
|
+
echo "{\\"error\\": \\"SOVR: This action requires human approval. Reason: $REASON\\"}"
|
|
153
|
+
exit 2
|
|
154
|
+
;;
|
|
155
|
+
*)
|
|
156
|
+
debug "UNKNOWN verdict: $VERDICT, fail-$SOVR_FAIL_MODE"
|
|
157
|
+
exit $FAIL_EXIT
|
|
158
|
+
;;
|
|
159
|
+
esac
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
function generateAuditHookScript(config) {
|
|
163
|
+
const endpoint = config.endpoint || "http://localhost:45557";
|
|
164
|
+
const apiKeyRef = config.apiKey ? `"${config.apiKey}"` : '"${SOVR_API_KEY:-}"';
|
|
165
|
+
return `#!/bin/bash
|
|
166
|
+
# SOVR PostToolUse Audit Hook for Claude Code
|
|
167
|
+
# Records tool execution results for audit trail
|
|
168
|
+
set -euo pipefail
|
|
169
|
+
|
|
170
|
+
SOVR_ENDPOINT="\${SOVR_ENDPOINT:-${endpoint}}"
|
|
171
|
+
SOVR_API_KEY=${apiKeyRef}
|
|
172
|
+
|
|
173
|
+
INPUT=$(cat)
|
|
174
|
+
TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4 2>/dev/null || echo "unknown")
|
|
175
|
+
|
|
176
|
+
# Fire-and-forget audit log
|
|
177
|
+
curl -s -X POST "$SOVR_ENDPOINT/api/hook/audit" \\
|
|
178
|
+
-H "Content-Type: application/json" \\
|
|
179
|
+
-H "X-SOVR-API-Key: $SOVR_API_KEY" \\
|
|
180
|
+
-d "{
|
|
181
|
+
\\"tool_name\\": \\"$TOOL_NAME\\",
|
|
182
|
+
\\"event\\": \\"PostToolUse\\",
|
|
183
|
+
\\"timestamp\\": $(date +%s)000
|
|
184
|
+
}" \\
|
|
185
|
+
--connect-timeout 2 --max-time 3 2>/dev/null &
|
|
186
|
+
|
|
187
|
+
# Always allow (audit is non-blocking)
|
|
188
|
+
exit 0
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
function generateHooksConfig(config) {
|
|
192
|
+
const hookScriptPath = getHookScriptPath();
|
|
193
|
+
const timeout = config.hookTimeout || DEFAULT_HOOK_TIMEOUT;
|
|
194
|
+
const hooks = {
|
|
195
|
+
PreToolUse: []
|
|
196
|
+
};
|
|
197
|
+
const patterns = config.toolPatterns || DEFAULT_TOOL_PATTERNS;
|
|
198
|
+
for (const pattern of patterns) {
|
|
199
|
+
hooks.PreToolUse.push({
|
|
200
|
+
matcher: { tool_name: pattern },
|
|
201
|
+
hooks: [{ command: hookScriptPath, timeout }]
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (config.addAuditHook) {
|
|
205
|
+
const auditScriptPath = getAuditHookScriptPath();
|
|
206
|
+
hooks.PostToolUse = patterns.map((pattern) => ({
|
|
207
|
+
matcher: { tool_name: pattern },
|
|
208
|
+
hooks: [{ command: auditScriptPath, timeout: 5 }]
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
return hooks;
|
|
212
|
+
}
|
|
213
|
+
function installHooks(config) {
|
|
214
|
+
const settingsPath = config.settingsPath || getDefaultSettingsPath();
|
|
215
|
+
const hookScriptPath = getHookScriptPath();
|
|
216
|
+
const auditScriptPath = config.addAuditHook ? getAuditHookScriptPath() : void 0;
|
|
217
|
+
const settingsDir = (0, import_node_path.dirname)(settingsPath);
|
|
218
|
+
const hooksDir = (0, import_node_path.dirname)(hookScriptPath);
|
|
219
|
+
(0, import_node_fs.mkdirSync)(settingsDir, { recursive: true });
|
|
220
|
+
(0, import_node_fs.mkdirSync)(hooksDir, { recursive: true });
|
|
221
|
+
(0, import_node_fs.writeFileSync)(hookScriptPath, generateHookScript(config), "utf-8");
|
|
222
|
+
(0, import_node_fs.chmodSync)(hookScriptPath, 493);
|
|
223
|
+
if (auditScriptPath) {
|
|
224
|
+
(0, import_node_fs.writeFileSync)(auditScriptPath, generateAuditHookScript(config), "utf-8");
|
|
225
|
+
(0, import_node_fs.chmodSync)(auditScriptPath, 493);
|
|
226
|
+
}
|
|
227
|
+
let existingSettings = {};
|
|
228
|
+
let backup;
|
|
229
|
+
if ((0, import_node_fs.existsSync)(settingsPath)) {
|
|
230
|
+
try {
|
|
231
|
+
const raw = (0, import_node_fs.readFileSync)(settingsPath, "utf-8");
|
|
232
|
+
existingSettings = JSON.parse(raw);
|
|
233
|
+
backup = `${settingsPath}.sovr-backup.${Date.now()}`;
|
|
234
|
+
(0, import_node_fs.writeFileSync)(backup, raw, "utf-8");
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const sovrHooks = generateHooksConfig(config);
|
|
239
|
+
const mergedHooks = {};
|
|
240
|
+
if (existingSettings.hooks) {
|
|
241
|
+
for (const [event, defs] of Object.entries(existingSettings.hooks)) {
|
|
242
|
+
mergedHooks[event] = defs.filter(
|
|
243
|
+
(d) => !d.hooks.some((h) => h.command.includes("sovr"))
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const [event, defs] of Object.entries(sovrHooks)) {
|
|
248
|
+
if (!mergedHooks[event]) mergedHooks[event] = [];
|
|
249
|
+
mergedHooks[event].push(...defs);
|
|
250
|
+
}
|
|
251
|
+
const updatedSettings = {
|
|
252
|
+
...existingSettings,
|
|
253
|
+
hooks: mergedHooks
|
|
254
|
+
};
|
|
255
|
+
(0, import_node_fs.writeFileSync)(settingsPath, JSON.stringify(updatedSettings, null, 2), "utf-8");
|
|
256
|
+
return {
|
|
257
|
+
settingsPath,
|
|
258
|
+
hookScriptPath,
|
|
259
|
+
auditScriptPath,
|
|
260
|
+
installed: true,
|
|
261
|
+
backup
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function uninstallHooks(config) {
|
|
265
|
+
const settingsPath = config?.settingsPath || getDefaultSettingsPath();
|
|
266
|
+
if (!(0, import_node_fs.existsSync)(settingsPath)) {
|
|
267
|
+
return { removed: false, settingsPath };
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const raw = (0, import_node_fs.readFileSync)(settingsPath, "utf-8");
|
|
271
|
+
const settings = JSON.parse(raw);
|
|
272
|
+
if (settings.hooks) {
|
|
273
|
+
for (const [event, defs] of Object.entries(settings.hooks)) {
|
|
274
|
+
settings.hooks[event] = defs.filter(
|
|
275
|
+
(d) => !d.hooks.some((h) => h.command.includes("sovr"))
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
(0, import_node_fs.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
280
|
+
return { removed: true, settingsPath };
|
|
281
|
+
} catch {
|
|
282
|
+
return { removed: false, settingsPath };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function getDefaultSettingsPath() {
|
|
286
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".claude", "settings.json");
|
|
287
|
+
}
|
|
288
|
+
function getHookScriptPath() {
|
|
289
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".sovr", "hooks", "sovr-pretool-hook.sh");
|
|
290
|
+
}
|
|
291
|
+
function getAuditHookScriptPath() {
|
|
292
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".sovr", "hooks", "sovr-posttool-audit.sh");
|
|
293
|
+
}
|
|
294
|
+
function detectClaudeCode() {
|
|
295
|
+
const defaultPath = getDefaultSettingsPath();
|
|
296
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.dirname)(defaultPath))) {
|
|
297
|
+
return {
|
|
298
|
+
found: true,
|
|
299
|
+
settingsPath: defaultPath
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const altPaths = [
|
|
303
|
+
(0, import_node_path.join)((0, import_node_os.homedir)(), ".config", "claude", "settings.json"),
|
|
304
|
+
(0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Application Support", "Claude", "settings.json")
|
|
305
|
+
];
|
|
306
|
+
for (const p of altPaths) {
|
|
307
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.dirname)(p))) {
|
|
308
|
+
return { found: true, settingsPath: p };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return { found: false };
|
|
312
|
+
}
|
|
313
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
314
|
+
0 && (module.exports = {
|
|
315
|
+
detectClaudeCode,
|
|
316
|
+
generateAuditHookScript,
|
|
317
|
+
generateHookScript,
|
|
318
|
+
generateHooksConfig,
|
|
319
|
+
installHooks,
|
|
320
|
+
uninstallHooks
|
|
321
|
+
});
|