@sanctix/client 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/cli/status.js ADDED
@@ -0,0 +1,103 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { httpRequestJson } from './runtime.js';
5
+
6
+ export async function status() {
7
+ const cwd = process.cwd();
8
+ const manifestPath = path.join(cwd, '.sanctix/sanctix.config.json');
9
+
10
+ console.log(chalk.bold('Sanctix Status'));
11
+
12
+ if (!(await fs.pathExists(manifestPath))) {
13
+ console.log('Not initialized — run sanctix init to get started.');
14
+ process.exit(0);
15
+ }
16
+
17
+ const manifest = await fs.readJSON(manifestPath);
18
+ const auditUrl = manifest.auditUrl;
19
+ const runtime = manifest.runtime;
20
+
21
+ // Step 2: audit service health.
22
+ const health = await httpRequestJson('GET', auditUrl.replace(/\/$/, '') + '/health', { timeoutMs: 3000 });
23
+ let auditStatus;
24
+ if (health.status === 200) {
25
+ auditStatus = chalk.green('connected');
26
+ } else if (health.error) {
27
+ auditStatus = chalk.red('disconnected (' + health.error + ')');
28
+ } else {
29
+ auditStatus = chalk.red('disconnected (HTTP ' + health.status + ')');
30
+ }
31
+
32
+ // Step 3: hook files.
33
+ const managedFiles = manifest.managedFiles || [];
34
+ let missingCount = 0;
35
+ for (const rel of managedFiles) {
36
+ if (!(await fs.pathExists(path.join(cwd, rel)))) missingCount++;
37
+ }
38
+ let hookStatus;
39
+ if (managedFiles.length === 0) {
40
+ hookStatus = 'n/a';
41
+ } else if (missingCount === 0) {
42
+ hookStatus = chalk.green('active');
43
+ } else if (missingCount === managedFiles.length) {
44
+ hookStatus = chalk.red('not installed');
45
+ } else {
46
+ hookStatus = chalk.yellow('partial (' + missingCount + ' missing)');
47
+ }
48
+
49
+ // Step 4: server-side fields.
50
+ let sessionsToday = 'n/a';
51
+ let trustLevel = 'n/a';
52
+ let chainStatus = 'n/a';
53
+ if (health.status === 200) {
54
+ const statusRes = await httpRequestJson('GET', auditUrl.replace(/\/$/, '') + '/status', { timeoutMs: 3000 });
55
+ if (statusRes.status === 200 && statusRes.body && typeof statusRes.body === 'object') {
56
+ if (statusRes.body.sessions_today !== undefined) sessionsToday = String(statusRes.body.sessions_today);
57
+ if (statusRes.body.trust_level !== undefined) trustLevel = String(statusRes.body.trust_level);
58
+ if (statusRes.body.chain_status !== undefined) chainStatus = String(statusRes.body.chain_status);
59
+ }
60
+ }
61
+
62
+ // Step 5: print.
63
+ console.log('Audit service: ' + auditStatus);
64
+ console.log('Hooks: ' + hookStatus + ' (' + runtime + ')');
65
+ console.log('Sessions today: ' + sessionsToday);
66
+ console.log('Trust level: ' + trustLevel);
67
+ console.log('Chain: ' + chainStatus);
68
+
69
+ // Step 6: active session.
70
+ const sessionEnvPath = path.join(cwd, '.sanctix/session.env');
71
+ if (await fs.pathExists(sessionEnvPath)) {
72
+ const text = await fs.readFile(sessionEnvPath, 'utf8');
73
+ const sessionEnv = parseEnvFile(text);
74
+ if (sessionEnv.AWF_CORRELATION_ID) {
75
+ console.log('Active session: ' + sessionEnv.AWF_CORRELATION_ID);
76
+ console.log('Task: ' + (sessionEnv.SANCTIX_TASK || ''));
77
+ console.log('Risk: ' + (sessionEnv.SANCTIX_RISK || 'unknown'));
78
+ console.log('Started: ' + (sessionEnv.SANCTIX_SESSION_STARTED || ''));
79
+ } else {
80
+ console.log('Active session: none (run sanctix start "task description")');
81
+ }
82
+ } else {
83
+ console.log('Active session: none (run sanctix start "task description")');
84
+ }
85
+
86
+ // Suggested fix.
87
+ if (health.status !== 200) {
88
+ console.log('');
89
+ console.log(chalk.yellow('Fix: start the audit service at ' + auditUrl + ' or update .sanctix/sanctix.config.json.'));
90
+ } else if (missingCount > 0) {
91
+ console.log('');
92
+ console.log(chalk.yellow('Fix: hook files are missing — run sanctix init to reinstall.'));
93
+ }
94
+ }
95
+
96
+ function parseEnvFile(text) {
97
+ const out = {};
98
+ for (const line of text.split('\n')) {
99
+ const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
100
+ if (m) out[m[1]] = m[2];
101
+ }
102
+ return out;
103
+ }
package/cli/stop.js ADDED
@@ -0,0 +1,134 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { httpRequestJson } from './runtime.js';
5
+
6
+ export async function stop(_options = {}) {
7
+ const cwd = process.cwd();
8
+
9
+ // Step 1: Read session env file.
10
+ const sessionEnvPath = path.join(cwd, '.sanctix/session.env');
11
+ if (!(await fs.pathExists(sessionEnvPath))) {
12
+ console.log('No active Sanctix session found.');
13
+ process.exit(0);
14
+ }
15
+
16
+ // Step 2: Parse session env.
17
+ const text = await fs.readFile(sessionEnvPath, 'utf8');
18
+ const env = parseEnvFile(text);
19
+ const correlationId = env.AWF_CORRELATION_ID;
20
+ const sessionId = env.AWF_SESSION_ID;
21
+ const task = env.SANCTIX_TASK || '';
22
+ const risk = env.SANCTIX_RISK || 'unknown';
23
+ const startedAt = env.SANCTIX_SESSION_STARTED;
24
+
25
+ // Step 3: Read manifest.
26
+ const manifestPath = path.join(cwd, '.sanctix/sanctix.config.json');
27
+ if (!(await fs.pathExists(manifestPath))) {
28
+ console.error(chalk.red('Sanctix manifest missing. Cannot complete session cleanly.'));
29
+ process.exit(1);
30
+ }
31
+ const manifest = await fs.readJSON(manifestPath);
32
+ const auditUrl = manifest.auditUrl.replace(/\/$/, '');
33
+ const apiKey = process.env.SANCTIX_API_KEY || readEnvKey(cwd, 'SANCTIX_API_KEY');
34
+
35
+ // Step 4: Get session summary from audit service.
36
+ const sessionRes = await httpRequestJson('GET', auditUrl + '/session/' + correlationId, {
37
+ apiKey: apiKey || null,
38
+ timeoutMs: 3000,
39
+ });
40
+ let summaryLine = null;
41
+ if (sessionRes.status === 200 && sessionRes.body && typeof sessionRes.body === 'object') {
42
+ const count = sessionRes.body.event_count;
43
+ const chain = sessionRes.body.chain_status;
44
+ summaryLine = 'Events: ' + (count !== undefined ? count : 'n/a') +
45
+ (chain ? ' (chain: ' + chain + ')' : '');
46
+ } else {
47
+ summaryLine = 'Session data not yet available.';
48
+ }
49
+
50
+ // Step 5: Post SANCTIX_SESSION_COMPLETED event.
51
+ const completedAt = new Date().toISOString();
52
+ const userId = process.env.USER || process.env.USERNAME || 'user';
53
+ const postRes = await httpRequestJson('POST', auditUrl + '/events', {
54
+ apiKey: apiKey || null,
55
+ timeoutMs: 3000,
56
+ body: {
57
+ actor_type: 'agent',
58
+ event_type: 'sanctix.session.completed',
59
+ timestamp_utc: completedAt,
60
+ correlation_id: correlationId,
61
+ user_id: userId,
62
+ runtime_provider: manifest.runtime,
63
+ event_data: {
64
+ session_id: sessionId,
65
+ task: task,
66
+ risk: risk,
67
+ started_at: startedAt,
68
+ completed_at: completedAt,
69
+ },
70
+ },
71
+ });
72
+ if (postRes.status !== 201 && postRes.status !== 200) {
73
+ console.warn(chalk.yellow(
74
+ 'Warning: failed to post session.completed event' +
75
+ (postRes.error ? ' (' + postRes.error + ')' : ' (HTTP ' + postRes.status + ')')
76
+ ));
77
+ }
78
+
79
+ // Step 6: Remove session env file.
80
+ await fs.remove(sessionEnvPath);
81
+
82
+ // Step 7: Print summary.
83
+ const duration = formatDuration(startedAt, completedAt);
84
+ console.log('');
85
+ console.log(chalk.bold('=== Sanctix Session Complete ==='));
86
+ console.log('Session: ' + correlationId);
87
+ console.log('Task: ' + task);
88
+ console.log('Risk: ' + risk);
89
+ console.log('Duration: ' + duration);
90
+ if (summaryLine) console.log(summaryLine);
91
+ console.log('');
92
+ console.log('To view this session:');
93
+ console.log(' awf audit show --correlation-id ' + correlationId);
94
+ console.log('');
95
+ console.log(chalk.bold('================================'));
96
+ }
97
+
98
+ function parseEnvFile(text) {
99
+ const out = {};
100
+ for (const line of text.split('\n')) {
101
+ const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
102
+ if (m) out[m[1]] = m[2];
103
+ }
104
+ return out;
105
+ }
106
+
107
+ function formatDuration(startedAt, completedAt) {
108
+ try {
109
+ const ms = new Date(completedAt).getTime() - new Date(startedAt).getTime();
110
+ if (!isFinite(ms) || ms < 0) return 'n/a';
111
+ const totalSec = Math.floor(ms / 1000);
112
+ const h = Math.floor(totalSec / 3600);
113
+ const m = Math.floor((totalSec % 3600) / 60);
114
+ const s = totalSec % 60;
115
+ if (h > 0) return `${h}h ${m}m ${s}s`;
116
+ if (m > 0) return `${m}m ${s}s`;
117
+ return `${s}s`;
118
+ } catch (_e) {
119
+ return 'n/a';
120
+ }
121
+ }
122
+
123
+ function readEnvKey(cwd, key) {
124
+ try {
125
+ const envPath = path.join(cwd, '.env');
126
+ if (!fs.pathExistsSync(envPath)) return null;
127
+ const text = fs.readFileSync(envPath, 'utf8');
128
+ for (const line of text.split('\n')) {
129
+ const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
130
+ if (m && m[1] === key && m[2]) return m[2];
131
+ }
132
+ } catch (_e) {}
133
+ return null;
134
+ }
@@ -0,0 +1,195 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import inquirer from 'inquirer';
4
+ import chalk from 'chalk';
5
+ import { SANCTIX_HOOK_COMMANDS } from './init.js';
6
+
7
+ export async function uninstall(options) {
8
+ const cwd = process.cwd();
9
+ const manifestPath = path.join(cwd, '.sanctix/sanctix.config.json');
10
+
11
+ // Step 1: Read manifest.
12
+ if (!(await fs.pathExists(manifestPath))) {
13
+ console.log('Sanctix is not installed in this directory.');
14
+ console.log('Run sanctix init to install.');
15
+ process.exit(0);
16
+ }
17
+ const manifest = await fs.readJSON(manifestPath);
18
+
19
+ // Step 2: Confirm.
20
+ if (!options.yes) {
21
+ console.log('The following will be removed:');
22
+ for (const f of manifest.managedFiles || []) console.log(' ' + f);
23
+ for (const k of manifest.managedEnvKeys || []) console.log(' .env: ' + k);
24
+ console.log(' .sanctix/sanctix.config.json');
25
+ const answer = await inquirer.prompt([{
26
+ type: 'confirm',
27
+ name: 'cont',
28
+ message: 'Remove Sanctix from this repo?',
29
+ default: false,
30
+ }]);
31
+ if (!answer.cont) {
32
+ console.log('Aborted.');
33
+ process.exit(0);
34
+ }
35
+ }
36
+
37
+ const removed = [];
38
+
39
+ // Step 3: Remove hook files (and any shared helpers we installed).
40
+ for (const rel of manifest.managedFiles || []) {
41
+ const full = path.join(cwd, rel);
42
+ if (await fs.pathExists(full)) {
43
+ await fs.remove(full);
44
+ console.log('[removed] ' + rel);
45
+ removed.push(rel);
46
+ }
47
+ }
48
+
49
+ if (manifest.runtime === 'claude_code') {
50
+ // Remove shared helpers from each hook subdir.
51
+ for (const sub of ['pre-tool-use', 'post-tool-use', 'sub-agent-start']) {
52
+ for (const helper of ['tool-capture.cjs', 'post-audit-event.cjs']) {
53
+ const helperPath = path.join(cwd, '.claude/hooks', sub, helper);
54
+ if (await fs.pathExists(helperPath)) {
55
+ await fs.remove(helperPath);
56
+ }
57
+ }
58
+ // Remove empty hook subdir.
59
+ const subDir = path.join(cwd, '.claude/hooks', sub);
60
+ if (await fs.pathExists(subDir)) {
61
+ const entries = await fs.readdir(subDir);
62
+ if (entries.length === 0) await fs.remove(subDir);
63
+ }
64
+ }
65
+ // Remove empty .claude/hooks/ if no remaining children.
66
+ const hooksDir = path.join(cwd, '.claude/hooks');
67
+ if (await fs.pathExists(hooksDir)) {
68
+ const entries = await fs.readdir(hooksDir);
69
+ if (entries.length === 0) await fs.remove(hooksDir);
70
+ }
71
+
72
+ // Update settings.json — strip Sanctix entries.
73
+ const settingsPath = path.join(cwd, '.claude/settings.json');
74
+ if (await fs.pathExists(settingsPath)) {
75
+ const settings = await fs.readJSON(settingsPath);
76
+ if (settings.hooks) {
77
+ for (const [event, entries] of Object.entries(SANCTIX_HOOK_COMMANDS)) {
78
+ if (!settings.hooks[event]) continue;
79
+ const ourCommands = new Set(entries.map((e) => e.command));
80
+ settings.hooks[event] = settings.hooks[event].filter((block) => {
81
+ const hooks = block.hooks || [];
82
+ const isOurs = hooks.length > 0 &&
83
+ hooks.every((h) => h && h.type === 'command' && ourCommands.has(h.command));
84
+ return !isOurs;
85
+ });
86
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
87
+ }
88
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
89
+ }
90
+ if (Object.keys(settings).length === 0) {
91
+ await fs.remove(settingsPath);
92
+ } else {
93
+ await fs.writeJSON(settingsPath, settings, { spaces: 2 });
94
+ }
95
+ }
96
+ }
97
+
98
+ // Step 4: Remove .env keys.
99
+ const envPath = path.join(cwd, '.env');
100
+ if (await fs.pathExists(envPath)) {
101
+ const text = await fs.readFile(envPath, 'utf8');
102
+ const managedKeys = new Set(manifest.managedEnvKeys || []);
103
+ const kept = text.split('\n').filter((line) => {
104
+ const m = line.match(/^([A-Z0-9_]+)=/);
105
+ if (m && managedKeys.has(m[1])) {
106
+ console.log('[removed] .env: ' + m[1]);
107
+ removed.push('.env:' + m[1]);
108
+ return false;
109
+ }
110
+ return true;
111
+ });
112
+ let out = kept.join('\n').replace(/\n{3,}/g, '\n\n');
113
+ if (out.trim() === '') {
114
+ await fs.remove(envPath);
115
+ } else {
116
+ if (!out.endsWith('\n')) out += '\n';
117
+ await fs.writeFile(envPath, out);
118
+ }
119
+ }
120
+
121
+ // Step 5: Runtime-specific cleanup.
122
+ if (manifest.runtime === 'codex') {
123
+ const agentsPath = path.join(cwd, 'AGENTS.md');
124
+ if (await fs.pathExists(agentsPath)) {
125
+ const text = await fs.readFile(agentsPath, 'utf8');
126
+ const cleaned = stripSanctixSection(text);
127
+ if (cleaned.trim() === '') {
128
+ await fs.remove(agentsPath);
129
+ } else {
130
+ await fs.writeFile(agentsPath, cleaned);
131
+ }
132
+ }
133
+ }
134
+ if (manifest.runtime === 'cursor') {
135
+ const hooksJsonPath = path.join(cwd, '.cursor/hooks.json');
136
+ if (await fs.pathExists(hooksJsonPath)) {
137
+ const json = await fs.readJSON(hooksJsonPath);
138
+ delete json.sanctix;
139
+ if (Object.keys(json).length === 0) {
140
+ await fs.remove(hooksJsonPath);
141
+ } else {
142
+ await fs.writeJSON(hooksJsonPath, json, { spaces: 2 });
143
+ }
144
+ }
145
+ }
146
+
147
+ // Step 6: Remove session env if present.
148
+ const sessionEnvPath = path.join(cwd, '.sanctix/session.env');
149
+ if (await fs.pathExists(sessionEnvPath)) {
150
+ await fs.remove(sessionEnvPath);
151
+ console.log('[removed] .sanctix/session.env');
152
+ removed.push('.sanctix/session.env');
153
+ }
154
+
155
+ // Step 7: Remove manifest.
156
+ await fs.remove(manifestPath);
157
+ const sanctixDir = path.join(cwd, '.sanctix');
158
+ if (await fs.pathExists(sanctixDir)) {
159
+ const entries = await fs.readdir(sanctixDir);
160
+ if (entries.length === 0) await fs.remove(sanctixDir);
161
+ }
162
+
163
+ // Step 7: Confirmation.
164
+ console.log('');
165
+ console.log(chalk.green('[ok] Sanctix removed from this repo.'));
166
+ console.log('');
167
+ console.log(' The following were removed:');
168
+ for (const r of removed) console.log(' ' + r);
169
+ console.log(' .sanctix/sanctix.config.json');
170
+ console.log('');
171
+ console.log(' Your repo is clean.');
172
+ }
173
+
174
+ function stripSanctixSection(text) {
175
+ const lines = text.split('\n');
176
+ const out = [];
177
+ let skipping = false;
178
+ for (const line of lines) {
179
+ if (line.startsWith('## Sanctix Governance')) {
180
+ skipping = true;
181
+ continue;
182
+ }
183
+ if (skipping) {
184
+ // Stop skipping at the next markdown header or document separator.
185
+ if (line.startsWith('## ') || line.startsWith('# ') || line === '---') {
186
+ skipping = false;
187
+ out.push(line);
188
+ continue;
189
+ }
190
+ continue;
191
+ }
192
+ out.push(line);
193
+ }
194
+ return out.join('\n').replace(/\n{3,}/g, '\n\n');
195
+ }
package/hooks/.gitkeep ADDED
File without changes
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+
8
+ const { captureToolContext } = require(path.join(__dirname, 'tool-capture.cjs'));
9
+ const { postAuditEvent } = require(path.join(__dirname, 'post-audit-event.cjs'));
10
+
11
+ const AUDIT_URL = process.env.SANCTIX_AUDIT_URL || 'http://localhost:8787';
12
+ const API_KEY = process.env.SANCTIX_API_KEY || null;
13
+ const CORRELATION_ID = process.env.AWF_CORRELATION_ID || crypto.randomUUID();
14
+ const USER_ID = process.env.AWF_USER_ID || 'user';
15
+
16
+ const LOG_PATH = '/tmp/sanctix-audit.log';
17
+
18
+ let input = {};
19
+ try {
20
+ input = JSON.parse(fs.readFileSync(0, 'utf8'));
21
+ } catch (_e) {
22
+ input = {};
23
+ }
24
+
25
+ const timestamp = new Date().toISOString();
26
+
27
+ try {
28
+ fs.appendFileSync(LOG_PATH, JSON.stringify({
29
+ timestamp,
30
+ tool: (input && input.tool_name) || 'Agent',
31
+ hook: 'post-tool-use',
32
+ decision: 'allow',
33
+ }) + '\n');
34
+ } catch (_e) {}
35
+
36
+ (async () => {
37
+ let toolCtx = null;
38
+ try { toolCtx = captureToolContext(input); } catch (_e) {}
39
+
40
+ try {
41
+ await postAuditEvent({
42
+ actor_type: 'agent',
43
+ event_type: 'hook.agent_spawn.post_tool_use',
44
+ timestamp_utc: timestamp,
45
+ correlation_id: CORRELATION_ID,
46
+ user_id: USER_ID,
47
+ runtime_provider: 'claude_code',
48
+ event_data: {
49
+ tool_name: toolCtx && toolCtx.tool_name,
50
+ file_path: toolCtx && toolCtx.file_path,
51
+ command: toolCtx && toolCtx.command,
52
+ args_redacted: toolCtx && toolCtx.args_redacted,
53
+ },
54
+ before_state: toolCtx && toolCtx.before_state,
55
+ }, { url: AUDIT_URL, apiKey: API_KEY, timeoutMs: 500 });
56
+ } catch (_e) {}
57
+
58
+ process.stdout.write(JSON.stringify({ decision: 'allow' }));
59
+ process.exit(0);
60
+ })();
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+
8
+ const { captureToolContext } = require(path.join(__dirname, 'tool-capture.cjs'));
9
+ const { postAuditEvent } = require(path.join(__dirname, 'post-audit-event.cjs'));
10
+
11
+ const AUDIT_URL = process.env.SANCTIX_AUDIT_URL || 'http://localhost:8787';
12
+ const API_KEY = process.env.SANCTIX_API_KEY || null;
13
+ const CORRELATION_ID = process.env.AWF_CORRELATION_ID || crypto.randomUUID();
14
+ const USER_ID = process.env.AWF_USER_ID || 'user';
15
+
16
+ const LOG_PATH = '/tmp/sanctix-audit.log';
17
+
18
+ let input = {};
19
+ try {
20
+ input = JSON.parse(fs.readFileSync(0, 'utf8'));
21
+ } catch (_e) {
22
+ input = {};
23
+ }
24
+
25
+ const timestamp = new Date().toISOString();
26
+
27
+ try {
28
+ fs.appendFileSync(LOG_PATH, JSON.stringify({
29
+ timestamp,
30
+ tool: (input && input.tool_name) || 'Agent',
31
+ hook: 'pre-tool-use',
32
+ decision: 'allow',
33
+ }) + '\n');
34
+ } catch (_e) {}
35
+
36
+ (async () => {
37
+ let toolCtx = null;
38
+ try { toolCtx = captureToolContext(input); } catch (_e) {}
39
+
40
+ try {
41
+ await postAuditEvent({
42
+ actor_type: 'agent',
43
+ event_type: 'hook.agent_spawn.pre_tool_use',
44
+ timestamp_utc: timestamp,
45
+ correlation_id: CORRELATION_ID,
46
+ user_id: USER_ID,
47
+ runtime_provider: 'claude_code',
48
+ event_data: {
49
+ tool_name: toolCtx && toolCtx.tool_name,
50
+ file_path: toolCtx && toolCtx.file_path,
51
+ command: toolCtx && toolCtx.command,
52
+ args_redacted: toolCtx && toolCtx.args_redacted,
53
+ },
54
+ before_state: toolCtx && toolCtx.before_state,
55
+ }, { url: AUDIT_URL, apiKey: API_KEY, timeoutMs: 500 });
56
+ } catch (_e) {}
57
+
58
+ process.stdout.write(JSON.stringify({ decision: 'allow' }));
59
+ process.exit(0);
60
+ })();
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+
8
+ const { captureToolContext } = require(path.join(__dirname, 'tool-capture.cjs'));
9
+ const { postAuditEvent } = require(path.join(__dirname, 'post-audit-event.cjs'));
10
+
11
+ const AUDIT_URL = process.env.SANCTIX_AUDIT_URL || 'http://localhost:8787';
12
+ const API_KEY = process.env.SANCTIX_API_KEY || null;
13
+ const HOOK_MODE = process.env.SANCTIX_HOOK_MODE || 'enforce';
14
+ const CORRELATION_ID = process.env.AWF_CORRELATION_ID || crypto.randomUUID();
15
+ const USER_ID = process.env.AWF_USER_ID || 'user';
16
+
17
+ const LOG_PATH = '/tmp/sanctix-audit.log';
18
+
19
+ let input = {};
20
+ try {
21
+ input = JSON.parse(fs.readFileSync(0, 'utf8'));
22
+ } catch (_e) {
23
+ input = {};
24
+ }
25
+
26
+ const command = (input && input.tool_input && input.tool_input.command) || 'unknown';
27
+ const timestamp = new Date().toISOString();
28
+
29
+ const blocked = ['rm -rf /', 'DROP TABLE', 'delete from audit'];
30
+ const isBlocked = HOOK_MODE === 'enforce' &&
31
+ blocked.some((b) => command.toLowerCase().includes(b.toLowerCase()));
32
+
33
+ try {
34
+ fs.appendFileSync(LOG_PATH, JSON.stringify({
35
+ timestamp,
36
+ tool: 'Bash',
37
+ command: String(command).substring(0, 200),
38
+ hook: 'pre-tool-use',
39
+ decision: isBlocked ? 'block' : 'allow',
40
+ }) + '\n');
41
+ } catch (_e) {}
42
+
43
+ (async () => {
44
+ let toolCtx = null;
45
+ try { toolCtx = captureToolContext(input); } catch (_e) {}
46
+
47
+ try {
48
+ await postAuditEvent({
49
+ actor_type: 'agent',
50
+ event_type: 'hook.bash.pre_tool_use',
51
+ timestamp_utc: timestamp,
52
+ correlation_id: CORRELATION_ID,
53
+ user_id: USER_ID,
54
+ runtime_provider: 'claude_code',
55
+ event_data: {
56
+ tool_name: toolCtx && toolCtx.tool_name,
57
+ file_path: toolCtx && toolCtx.file_path,
58
+ command: toolCtx && toolCtx.command,
59
+ args_redacted: toolCtx && toolCtx.args_redacted,
60
+ },
61
+ before_state: toolCtx && toolCtx.before_state,
62
+ }, { url: AUDIT_URL, apiKey: API_KEY, timeoutMs: 500 });
63
+ } catch (_e) {}
64
+
65
+ if (isBlocked) {
66
+ process.stdout.write(JSON.stringify({
67
+ decision: 'block',
68
+ reason: 'Command blocked by Sanctix governance',
69
+ }));
70
+ process.exit(2);
71
+ }
72
+
73
+ process.stdout.write(JSON.stringify({ decision: 'allow' }));
74
+ process.exit(0);
75
+ })();
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+
8
+ const { captureToolContext } = require(path.join(__dirname, 'tool-capture.cjs'));
9
+ const { postAuditEvent } = require(path.join(__dirname, 'post-audit-event.cjs'));
10
+
11
+ const AUDIT_URL = process.env.SANCTIX_AUDIT_URL || 'http://localhost:8787';
12
+ const API_KEY = process.env.SANCTIX_API_KEY || null;
13
+ const CORRELATION_ID = process.env.AWF_CORRELATION_ID || crypto.randomUUID();
14
+ const USER_ID = process.env.AWF_USER_ID || 'user';
15
+
16
+ const LOG_PATH = '/tmp/sanctix-audit.log';
17
+
18
+ let input = {};
19
+ try {
20
+ input = JSON.parse(fs.readFileSync(0, 'utf8'));
21
+ } catch (_e) {
22
+ input = {};
23
+ }
24
+
25
+ const toolName = (input && input.tool_name) || '';
26
+ const filePath = (input && input.tool_input && (input.tool_input.path || input.tool_input.file_path)) || 'unknown';
27
+ const timestamp = new Date().toISOString();
28
+
29
+ try {
30
+ fs.appendFileSync(LOG_PATH, JSON.stringify({
31
+ timestamp,
32
+ tool: toolName,
33
+ file: filePath,
34
+ hook: 'post-tool-use',
35
+ decision: 'allow',
36
+ }) + '\n');
37
+ } catch (_e) {}
38
+
39
+ (async () => {
40
+ let toolCtx = null;
41
+ try { toolCtx = captureToolContext(input); } catch (_e) {}
42
+
43
+ try {
44
+ await postAuditEvent({
45
+ actor_type: 'agent',
46
+ event_type: 'hook.file_edit.post_tool_use',
47
+ timestamp_utc: timestamp,
48
+ correlation_id: CORRELATION_ID,
49
+ user_id: USER_ID,
50
+ runtime_provider: 'claude_code',
51
+ event_data: {
52
+ tool_name: toolCtx && toolCtx.tool_name,
53
+ file_path: toolCtx && toolCtx.file_path,
54
+ command: toolCtx && toolCtx.command,
55
+ args_redacted: toolCtx && toolCtx.args_redacted,
56
+ },
57
+ before_state: toolCtx && toolCtx.before_state,
58
+ }, { url: AUDIT_URL, apiKey: API_KEY, timeoutMs: 500 });
59
+ } catch (_e) {}
60
+
61
+ process.stdout.write(JSON.stringify({ decision: 'allow' }));
62
+ process.exit(0);
63
+ })();