@seanmozeik/tripwire 0.6.7 → 0.7.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -141
  3. package/dist/index.js +35 -0
  4. package/dist/tripwire-cli.js +2 -10
  5. package/dist/tripwire-hook.js +3 -0
  6. package/dist/tripwire-pi.js +4 -0
  7. package/dist/tripwire.js +135 -90
  8. package/dist/types/dispatch.d.ts +18 -0
  9. package/dist/types/index.d.ts +6 -0
  10. package/dist/types/lib/bash.d.ts +27 -0
  11. package/dist/types/lib/config.d.ts +110 -0
  12. package/dist/types/lib/cursor.d.ts +16 -0
  13. package/dist/types/lib/decision.d.ts +13 -0
  14. package/dist/types/lib/diff.d.ts +3 -0
  15. package/dist/types/lib/event.d.ts +45 -0
  16. package/dist/types/lib/log.d.ts +2 -0
  17. package/dist/types/lib/secrets.d.ts +41 -0
  18. package/dist/types/rules/bash-deny.d.ts +5 -0
  19. package/dist/types/rules/bash-git.d.ts +5 -0
  20. package/dist/types/rules/bash-network-install.d.ts +4 -0
  21. package/dist/types/rules/bash-redirect.d.ts +4 -0
  22. package/dist/types/rules/bash-scoped-rm.d.ts +5 -0
  23. package/dist/types/rules/bash-tar-explosion.d.ts +4 -0
  24. package/dist/types/rules/config-custom.d.ts +6 -0
  25. package/dist/types/rules/lazy-code.d.ts +4 -0
  26. package/dist/types/rules/path-protect.d.ts +12 -0
  27. package/dist/types/rules/post-secret-scrub.d.ts +12 -0
  28. package/dist/types/rules/read-protect.d.ts +4 -0
  29. package/dist/types/rules/tool-policy.d.ts +5 -0
  30. package/package.json +53 -22
  31. package/dist/tripwire-cli.js.jsc +0 -0
  32. package/dist/tripwire.js.jsc +0 -0
  33. package/src/cli.ts +0 -264
  34. package/src/dispatch.ts +0 -354
  35. package/src/index.ts +0 -6
  36. package/src/lib/bash.ts +0 -1284
  37. package/src/lib/config.ts +0 -127
  38. package/src/lib/decision.ts +0 -36
  39. package/src/lib/diff.ts +0 -26
  40. package/src/lib/event.ts +0 -106
  41. package/src/lib/install.ts +0 -238
  42. package/src/lib/log.ts +0 -24
  43. package/src/lib/secrets.ts +0 -121
  44. package/src/rules/bash-deny.ts +0 -394
  45. package/src/rules/bash-git.ts +0 -603
  46. package/src/rules/bash-network-install.ts +0 -72
  47. package/src/rules/bash-redirect.ts +0 -91
  48. package/src/rules/bash-scoped-rm.ts +0 -84
  49. package/src/rules/bash-tar-explosion.ts +0 -76
  50. package/src/rules/bash-tool-policy.ts +0 -146
  51. package/src/rules/config-custom.ts +0 -160
  52. package/src/rules/lazy-code.ts +0 -95
  53. package/src/rules/path-protect.ts +0 -68
  54. package/src/rules/post-secret-scrub.ts +0 -38
  55. package/src/rules/read-protect.ts +0 -67
package/src/cli.ts DELETED
@@ -1,264 +0,0 @@
1
- #!/usr/bin/env bun
2
- // `tripwire test '<command>'` — pipe a synthetic event through the
3
- // Dispatcher and pretty-print the decision. Indispensable for tuning
4
- // Rules without going through Claude Code.
5
- //
6
- // `tripwire install <target>` — install tripwire hooks for AI agents.
7
- //
8
- // Usage:
9
- // Bun src/cli.ts test 'rm -rf /etc'
10
- // Bun src/cli.ts test --tool=Read --path=.env
11
- // Bun src/cli.ts test --post --tool=Bash --stdout='ghp_<token>'
12
- // Bun src/cli.ts install claude
13
- // Bun src/cli.ts install codex
14
- // Bun src/cli.ts install pi
15
- // Bun src/cli.ts install all
16
-
17
- // oxlint-disable-next-line unicorn/import-style
18
- import { dirname } from 'node:path';
19
-
20
- import { BunServices } from '@effect/platform-bun';
21
- import { file } from 'bun';
22
- import { Effect, Option } from 'effect';
23
- import { Argument, Command, Flag } from 'effect/unstable/cli';
24
-
25
- import pkg from '../package.json' with { type: 'json' };
26
- import { installAll, installClaude, installCodex, installPi } from './lib/install';
27
-
28
- // Resolve tripwire-hook path at runtime using process.argv
29
- // This works in both script mode (bun run) and compiled/bundled mode
30
- const runtimeSelf = (): string => {
31
- const isBunCli = /\/bun(?<ext>\.exe)?$/.test(process.argv[0] ?? '');
32
- return isBunCli ? process.argv[1]! : process.argv[0]!;
33
- };
34
-
35
- const dispatchBin = async (): Promise<string> => {
36
- const cliPath = runtimeSelf();
37
- const cliDir = dirname(cliPath);
38
- // Try tripwire-hook in same directory first (installed scenario)
39
- const installedPath = `${cliDir}/tripwire-hook`;
40
- try {
41
- await file(installedPath).text();
42
- return installedPath;
43
- } catch {
44
- // Fallback to development scenario: tripwire-hook in ../dist relative to CLI
45
- return `${cliDir}/tripwire.js`;
46
- }
47
- };
48
-
49
- interface BuiltEvent {
50
- hook_event_name: string;
51
- tool_name: string;
52
- cwd: string;
53
- session_id: string;
54
- tool_input?: unknown;
55
- tool_response?: unknown;
56
- }
57
-
58
- const buildToolInput = (
59
- tool: string,
60
- command: string | undefined,
61
- path: string | undefined,
62
- content: string | undefined,
63
- ): unknown => {
64
- if (tool === 'Bash') {
65
- return { command: command ?? '' };
66
- }
67
- if (tool === 'Read') {
68
- return { file_path: path ?? '' };
69
- }
70
- if (tool === 'Write') {
71
- return { file_path: path ?? '', content: content ?? '' };
72
- }
73
- if (tool === 'Edit' || tool === 'MultiEdit') {
74
- return { file_path: path ?? '', old_string: '', new_string: content ?? '' };
75
- }
76
- return undefined;
77
- };
78
-
79
- interface EventParams {
80
- readonly tool: string;
81
- readonly post: boolean;
82
- readonly command: string | undefined;
83
- readonly path: string | undefined;
84
- readonly stdout: string | undefined;
85
- readonly stderr: string | undefined;
86
- readonly content: string | undefined;
87
- }
88
-
89
- const buildEvent = (params: EventParams): BuiltEvent => {
90
- const { tool, post, command, path, stdout, stderr, content } = params;
91
- const eventName = post ? 'PostToolUse' : 'PreToolUse';
92
- const event: BuiltEvent = {
93
- hook_event_name: eventName,
94
- tool_name: tool,
95
- cwd: process.cwd(),
96
- session_id: 'tripwire-cli-test',
97
- tool_input: buildToolInput(tool, command, path, content),
98
- };
99
- if (post) {
100
- event.tool_response =
101
- tool === 'Bash' ? { stdout: stdout ?? '', stderr: stderr ?? '' } : { content: content ?? '' };
102
- }
103
- return event;
104
- };
105
-
106
- const runTest = (config: {
107
- readonly command: string | undefined;
108
- readonly content: string | undefined;
109
- readonly path: string | undefined;
110
- readonly post: boolean;
111
- readonly stderr: string | undefined;
112
- readonly stdout: string | undefined;
113
- readonly tool: string;
114
- }): Effect.Effect<void> =>
115
- Effect.gen(function* () {
116
- const { command, content, path, post, stderr, stdout, tool } = config;
117
- const event = buildEvent({ tool, post, command, path, stdout, stderr, content });
118
- const bin = yield* Effect.promise(() => dispatchBin());
119
- const result = Bun.spawnSync([bin], {
120
- stdin: new TextEncoder().encode(JSON.stringify(event)),
121
- timeout: 10_000,
122
- stdout: 'pipe',
123
- stderr: 'pipe',
124
- });
125
- if (result.exitCode !== 0) {
126
- const errorOutput = new TextDecoder().decode(result.stderr);
127
- console.error(`error: ${errorOutput}`);
128
- process.exit(1);
129
- }
130
- const output = new TextDecoder().decode(result.stdout);
131
- try {
132
- const parsed = JSON.parse(output) as unknown;
133
- console.log(JSON.stringify(parsed, null, 2));
134
- } catch {
135
- console.log(output);
136
- }
137
- });
138
-
139
- const testCommand = Command.make(
140
- 'test',
141
- {
142
- command: Argument.string('command').pipe(
143
- Argument.optional,
144
- Argument.withDescription('Command to test (for Bash tool)'),
145
- ),
146
- content: Flag.string('content').pipe(
147
- Flag.optional,
148
- Flag.withDescription('Content for Write/Edit tools'),
149
- ),
150
- path: Flag.string('path').pipe(
151
- Flag.optional,
152
- Flag.withDescription('File path for Read/Write/Edit tools'),
153
- ),
154
- post: Flag.boolean('post').pipe(Flag.withDescription('Test PostToolUse instead of PreToolUse')),
155
- stderr: Flag.string('stderr').pipe(
156
- Flag.optional,
157
- Flag.withDescription('Stderr for PostToolUse Bash'),
158
- ),
159
- stdout: Flag.string('stdout').pipe(
160
- Flag.optional,
161
- Flag.withDescription('Stdout for PostToolUse Bash'),
162
- ),
163
- tool: Flag.string('tool').pipe(
164
- Flag.withDefault('Bash'),
165
- Flag.withDescription('Tool name (Bash, Read, Write, Edit, MultiEdit)'),
166
- ),
167
- },
168
- ({ command, content, path, post, stderr, stdout, tool }) =>
169
- runTest({
170
- command: Option.getOrUndefined(command),
171
- content: Option.getOrUndefined(content),
172
- path: Option.getOrUndefined(path),
173
- post,
174
- stderr: Option.getOrUndefined(stderr),
175
- stdout: Option.getOrUndefined(stdout),
176
- tool,
177
- }),
178
- ).pipe(Command.withDescription('Test a synthetic hook event'));
179
-
180
- const runInstall = (target: string): Effect.Effect<void> =>
181
- Effect.gen(function* () {
182
- if (!['claude', 'codex', 'pi', 'all'].includes(target)) {
183
- console.error(`error: unknown target "${target}"`);
184
- console.error('Valid targets: claude, codex, pi, all');
185
- process.exit(1);
186
- }
187
-
188
- let results: {
189
- readonly target: string;
190
- readonly result: { readonly success: boolean; readonly message: string };
191
- }[];
192
-
193
- switch (target) {
194
- case 'claude': {
195
- const result = yield* Effect.promise(() => installClaude());
196
- results = [{ target: 'claude', result }];
197
- break;
198
- }
199
- case 'codex': {
200
- const result = yield* Effect.promise(() => installCodex());
201
- results = [{ target: 'codex', result }];
202
- break;
203
- }
204
- case 'pi': {
205
- const result = yield* Effect.promise(() => installPi());
206
- results = [{ target: 'pi', result }];
207
- break;
208
- }
209
- case 'all': {
210
- const installResults = yield* Effect.promise(() => installAll());
211
- results = installResults.map((r) => ({ target: r.target, result: r }));
212
- break;
213
- }
214
- default: {
215
- results = [];
216
- break;
217
- }
218
- }
219
-
220
- let hasFailure = false;
221
- for (const { target: t, result: r } of results) {
222
- if (r.success) {
223
- const symbol = r.message.startsWith('Already configured') ? '⊙' : '✓';
224
- console.log(`${symbol} [${t}] ${r.message}`);
225
- } else {
226
- console.error(`✗ [${t}] ${r.message}`);
227
- hasFailure = true;
228
- }
229
- }
230
-
231
- if (hasFailure) {
232
- process.exit(1);
233
- }
234
- });
235
-
236
- const installCommand = Command.make(
237
- 'install',
238
- {
239
- target: Argument.string('target').pipe(
240
- Argument.withDescription('Target agent (claude, codex, pi, or all)'),
241
- ),
242
- },
243
- ({ target }) => runInstall(target),
244
- ).pipe(Command.withDescription('Install tripwire hooks for AI agents'));
245
-
246
- const app = Command.make('tripwire').pipe(
247
- Command.withDescription('Opinionated hooks dispatcher for AI coding agents'),
248
- Command.withSubcommands([testCommand, installCommand]),
249
- );
250
-
251
- const program = Command.run(app, { version: pkg.version });
252
-
253
- const main = async (): Promise<void> => {
254
- try {
255
- await Effect.runPromise(program.pipe(Effect.provide(BunServices.layer)));
256
- } catch (error) {
257
- const message = error instanceof Error ? error.message : String(error);
258
- console.error(message);
259
- process.exitCode = 1;
260
- }
261
- };
262
-
263
- // oxlint-disable-next-line no-void, unicorn/prefer-top-level-await
264
- void main();
package/src/dispatch.ts DELETED
@@ -1,354 +0,0 @@
1
- #!/usr/bin/env bun
2
- // Tripwire — Claude Code hooks dispatcher.
3
- //
4
- // Reads a hook event JSON payload on stdin, routes by hook_event_name +
5
- // Tool_name, runs rules with per-rule timeouts, merges decisions
6
- // (most-restrictive wins), scans PostToolUse output for secrets via
7
- // Betterleaks, and writes Claude Code's expected JSON response on stdout.
8
- //
9
- // Design rules:
10
- // - A buggy or slow rule must never block the agent. Every rule runs
11
- // Under a timeout; any defect or timeout collapses to `allow`, logged.
12
- // - Block messages address the agent in second person and name the
13
- // Concrete alternative tool / approach. No vague "denied for safety".
14
- // - One bypass token: `tripwire-allow` (any comment syntax) on a code
15
- // Line, or `# tripwire-allow` in a bash command.
16
-
17
- import { BunRuntime } from '@effect/platform-bun';
18
- import { Cause, Effect, Exit, Schema } from 'effect';
19
-
20
- import { parseCommand } from './lib/bash';
21
- import {
22
- CONFIG_PATH,
23
- getDefaultConfig,
24
- loadConfigResult,
25
- mergeWithDefaults,
26
- type Config,
27
- } from './lib/config';
28
- import { type Decision, allow, deny, merge } from './lib/decision';
29
- import {
30
- type BashInput,
31
- type EditInput,
32
- type HookEvent,
33
- HookEventSchema,
34
- type ReadInput,
35
- type WriteInput,
36
- isBashInput,
37
- isEditInput,
38
- isReadInput,
39
- isWriteInput,
40
- } from './lib/event.ts';
41
- import { logError } from './lib/log';
42
- import { bashDeny } from './rules/bash-deny';
43
- import { bashGit } from './rules/bash-git';
44
- import { bashNetworkInstall } from './rules/bash-network-install';
45
- import { bashRedirect } from './rules/bash-redirect';
46
- import { bashScopedRm } from './rules/bash-scoped-rm';
47
- import { bashTarExplosion } from './rules/bash-tar-explosion';
48
- import { bashToolPolicy } from './rules/bash-tool-policy';
49
- import { configCustom } from './rules/config-custom';
50
- import { lazyCode } from './rules/lazy-code';
51
- import { pathProtect } from './rules/path-protect';
52
- import { postSecretScrub } from './rules/post-secret-scrub';
53
- import { readProtect } from './rules/read-protect';
54
-
55
- const readStdin = async (): Promise<string> => {
56
- const chunks: Buffer[] = [];
57
- for await (const chunk of process.stdin) {
58
- chunks.push(chunk as Buffer);
59
- }
60
- return Buffer.concat(chunks).toString('utf8');
61
- };
62
-
63
- const writeAllow = (): void => {
64
- process.stdout.write('{"continue": true}\n');
65
- };
66
-
67
- // Codex's PreToolUse hook rejects `hookSpecificOutput.additionalContext`
68
- // (openai/codex issue #19385). Detect Codex via its `turn_id` extension
69
- // And downgrade output accordingly. Claude Code accepts it, so we only
70
- // Narrow when we can confirm we're on Codex.
71
- const isCodex = (event: HookEvent): boolean => event.turn_id !== undefined;
72
-
73
- interface WarnOutput {
74
- hookEventName: string;
75
- additionalContext?: string;
76
- }
77
-
78
- const writeWarn = (event: HookEvent, decision: Decision): void => {
79
- const eventName = event.hook_event_name;
80
- const reason = `[tripwire:${decision.rule}] ${decision.message}`;
81
- if (isCodex(event)) {
82
- // Codex rejects `additionalContext` on PreToolUse. Send only
83
- // `systemMessage`.
84
- process.stdout.write(`${JSON.stringify({ continue: true, systemMessage: reason })}\n`);
85
- return;
86
- }
87
- const hookSpecificOutput: WarnOutput = { hookEventName: eventName, additionalContext: reason };
88
- process.stdout.write(`${JSON.stringify({ continue: true, hookSpecificOutput })}\n`);
89
- };
90
-
91
- const writePreToolGate = (eventName: string, decision: Decision): void => {
92
- const out = {
93
- hookSpecificOutput: {
94
- hookEventName: eventName,
95
- permissionDecision: decision.kind === 'deny' ? 'deny' : 'ask',
96
- permissionDecisionReason: `[tripwire:${decision.rule}] ${decision.message}`,
97
- },
98
- };
99
- process.stdout.write(`${JSON.stringify(out)}\n`);
100
- };
101
-
102
- const writePostToolBlock = (decision: Decision): void => {
103
- const out = {
104
- continue: true,
105
- decision: 'block',
106
- reason: `[tripwire:${decision.rule}] ${decision.message}`,
107
- };
108
- process.stdout.write(`${JSON.stringify(out)}\n`);
109
- };
110
-
111
- // Tool names vary across hosts. Claude Code uses `Bash`/`Read`/`Write`/
112
- // `Edit`/`MultiEdit`. Codex sends `apply_patch` for file edits. Devin sends
113
- // `exec` for shell. Pi (via pi-hooks) sends lowercase `bash`/`read`/`write`/
114
- // `edit`. Normalize everything to the Claude vocabulary so the rest of the
115
- // Dispatcher only deals with one set of names.
116
- const normalizeToolName = (name: string): string => {
117
- const n = name.toLowerCase();
118
- if (n === 'bash' || n === 'exec' || n === 'shell' || n === 'run_command') {
119
- return 'Bash';
120
- }
121
- if (n === 'read' || n === 'read_file') {
122
- return 'Read';
123
- }
124
- if (n === 'write' || n === 'write_file') {
125
- return 'Write';
126
- }
127
- if (n === 'edit' || n === 'edit_file' || n === 'multiedit' || n === 'apply_patch') {
128
- return 'Edit';
129
- }
130
- if (n === 'webfetch' || n === 'web_fetch' || n === 'fetch') {
131
- return 'WebFetch';
132
- }
133
- return name;
134
- };
135
-
136
- type RuleFn = () => Decision;
137
-
138
- const runRule = (name: string, fn: RuleFn, timeoutMs: number): Effect.Effect<Decision> =>
139
- Effect.gen(function* () {
140
- const exit = yield* Effect.exit(
141
- Effect.try({ try: fn, catch: (e) => e }).pipe(Effect.timeout(timeoutMs)),
142
- );
143
- if (Exit.isSuccess(exit)) {
144
- return exit.value;
145
- }
146
- logError(name, Cause.pretty(exit.cause));
147
- return allow(name);
148
- });
149
-
150
- interface Rule {
151
- readonly name: string;
152
- readonly fn: RuleFn;
153
- }
154
-
155
- const collectPreToolUseRules = (tool: string, input: unknown, config: Config): Rule[] => {
156
- const rules: Rule[] = [];
157
- if (tool === 'Bash' && isBashInput(input)) {
158
- const i: BashInput = input;
159
- const segments = parseCommand(i.command);
160
- rules.push({ name: 'bash-deny', fn: () => bashDeny(segments, i.command) });
161
- rules.push({
162
- name: 'bash-git',
163
- fn: () => bashGit(segments, i.command, config.git ?? { enforceConventionalCommits: true }),
164
- });
165
- rules.push({
166
- name: 'bash-scoped-rm',
167
- fn: () => bashScopedRm(segments, i.command, config.safePaths ?? {}),
168
- });
169
- rules.push({ name: 'bash-redirect', fn: () => bashRedirect(segments, i.command) });
170
- rules.push({ name: 'bash-network-install', fn: () => bashNetworkInstall(segments, i.command) });
171
- rules.push({ name: 'bash-tar-explosion', fn: () => bashTarExplosion(segments, i.command) });
172
- rules.push({ name: 'bash-tool-policy', fn: () => bashToolPolicy(segments, i.command) });
173
- rules.push({
174
- name: 'config-custom',
175
- fn: () =>
176
- configCustom(
177
- segments,
178
- i.command,
179
- config.blockedCommands ?? [],
180
- config.allowedCommands ?? [],
181
- ),
182
- });
183
- return rules;
184
- }
185
- if (tool === 'Read' && isReadInput(input)) {
186
- const i: ReadInput = input;
187
- rules.push({ name: 'read-protect', fn: () => readProtect(i) });
188
- return rules;
189
- }
190
- const isEdit = (tool === 'Edit' || tool === 'MultiEdit') && isEditInput(input);
191
- const isWrite = tool === 'Write' && isWriteInput(input);
192
- if (isEdit) {
193
- const i: EditInput = input;
194
- rules.push({ name: 'path-protect', fn: () => pathProtect(i) });
195
- rules.push({ name: 'lazy-code', fn: () => lazyCode(i) });
196
- } else if (isWrite) {
197
- const i: WriteInput = input;
198
- rules.push({ name: 'path-protect', fn: () => pathProtect(i) });
199
- rules.push({ name: 'lazy-code', fn: () => lazyCode(i) });
200
- }
201
- return rules;
202
- };
203
-
204
- const collectPostToolUseRules = (tool: string, response: unknown): Rule[] => {
205
- if (tool === 'Bash' || tool === 'Read' || tool === 'WebFetch') {
206
- return [{ name: 'post-secret-scrub', fn: () => postSecretScrub({ toolName: tool, response }) }];
207
- }
208
- return [];
209
- };
210
-
211
- const runRules = (rules: readonly Rule[], timeoutMs: number): Effect.Effect<Decision> =>
212
- Effect.gen(function* () {
213
- if (rules.length === 0) {
214
- return allow('no-rules');
215
- }
216
- const decisions: Decision[] = [];
217
- for (const r of rules) {
218
- decisions.push(yield* runRule(r.name, r.fn, timeoutMs));
219
- }
220
- return merge(decisions);
221
- });
222
-
223
- const runRulesSync = (rules: readonly Rule[]): Decision => {
224
- if (rules.length === 0) {
225
- return allow('no-rules');
226
- }
227
- return merge(rules.map((r) => r.fn()));
228
- };
229
-
230
- const decide = (event: HookEvent, config: Config = getDefaultConfig()): Decision => {
231
- const mergedConfig = mergeWithDefaults(config);
232
- const tool = normalizeToolName(event.tool_name ?? '');
233
- if (event.hook_event_name === 'PreToolUse') {
234
- return runRulesSync(collectPreToolUseRules(tool, event.tool_input, mergedConfig));
235
- }
236
- if (event.hook_event_name === 'PostToolUse') {
237
- return runRulesSync(collectPostToolUseRules(tool, event.tool_response));
238
- }
239
- return allow('no-rules');
240
- };
241
-
242
- const handleBashAllow = (event: HookEvent, decision: Decision, _config: Config): void => {
243
- if (decision.kind === 'warn') {
244
- writeWarn(event, decision);
245
- return;
246
- }
247
- writeAllow();
248
- };
249
-
250
- const handleAllow = (event: HookEvent, decision: Decision, config: Config): void => {
251
- const eventName = event.hook_event_name;
252
- const tool = normalizeToolName(event.tool_name ?? '');
253
- if (eventName === 'PreToolUse' && tool === 'Bash') {
254
- handleBashAllow(event, decision, config);
255
- return;
256
- }
257
- if (decision.kind === 'warn') {
258
- writeWarn(event, decision);
259
- return;
260
- }
261
- writeAllow();
262
- };
263
-
264
- // A broken config (bad JSON / unknown field / decode failure) silently dropping
265
- // All custom policy is the dangerous case this guards. Fail closed: deny the
266
- // Pending PreToolUse call with the decode error inline so the agent halts and
267
- // The user sees it, rather than running on bare defaults unannounced.
268
- const configErrorMessage = (error: string): string =>
269
- `tripwire config at ${CONFIG_PATH} failed to load, so ALL custom safety policy is ` +
270
- `inactive. Failing closed until it is fixed. Fix the JSON, then this clears on the next ` +
271
- `call (the shim daemon caches config at warm — restart it there). Error: ${error}`;
272
-
273
- const program = Effect.gen(function* () {
274
- const configLoad = yield* loadConfigResult();
275
- const raw = yield* Effect.promise(readStdin);
276
-
277
- const parseExit = yield* Effect.exit(
278
- Effect.try({ try: () => JSON.parse(raw) as unknown, catch: (e) => e }),
279
- );
280
- if (Exit.isFailure(parseExit)) {
281
- logError('parse', Cause.pretty(parseExit.cause));
282
- writeAllow();
283
- return;
284
- }
285
-
286
- const decodeExit = yield* Effect.exit(
287
- Schema.decodeUnknownEffect(HookEventSchema)(parseExit.value),
288
- );
289
- if (Exit.isFailure(decodeExit)) {
290
- logError('decode', Cause.pretty(decodeExit.cause));
291
- writeAllow();
292
- return;
293
- }
294
- const event = decodeExit.value;
295
-
296
- if (!configLoad.ok) {
297
- if (event.hook_event_name === 'PreToolUse') {
298
- writePreToolGate(
299
- event.hook_event_name,
300
- deny('config-error', configErrorMessage(configLoad.error)),
301
- );
302
- return;
303
- }
304
- // Config governs PreToolUse gating; PostToolUse secret-scrub is config-
305
- // Independent and there is always an imminent next PreToolUse to surface the
306
- // Deny, so don't block already-run output here.
307
- writeAllow();
308
- return;
309
- }
310
- const config = configLoad.config;
311
-
312
- if (event.hook_event_name === 'PreToolUse') {
313
- const decision = decide(event, config);
314
- if (decision.kind === 'deny' || decision.kind === 'ask') {
315
- writePreToolGate(event.hook_event_name, decision);
316
- return;
317
- }
318
- handleAllow(event, decision, config);
319
- return;
320
- }
321
-
322
- if (event.hook_event_name === 'PostToolUse') {
323
- const decision = decide(event, config);
324
- if (decision.kind === 'deny') {
325
- writePostToolBlock(decision);
326
- return;
327
- }
328
- writeAllow();
329
- return;
330
- }
331
-
332
- writeAllow();
333
- });
334
-
335
- const handled = program.pipe(
336
- Effect.catchCause((cause) => {
337
- logError('dispatch-fatal', Cause.pretty(cause));
338
- writeAllow();
339
- return Effect.void;
340
- }),
341
- );
342
-
343
- if (import.meta.main) {
344
- BunRuntime.runMain(handled);
345
- }
346
-
347
- export {
348
- collectPostToolUseRules,
349
- collectPreToolUseRules,
350
- decide,
351
- normalizeToolName,
352
- runRules,
353
- runRulesSync,
354
- };
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export type { Decision } from './lib/decision.ts';
2
- export type { HookEvent } from './lib/event.ts';
3
- export type { Config, ConfigLoad } from './lib/config.ts';
4
- export { allow, deny, ask, warn } from './lib/decision.ts';
5
- export { decide } from './dispatch.ts';
6
- export { getDefaultConfig, loadConfig, loadConfigResult, mergeWithDefaults } from './lib/config.ts';