@seanmozeik/tripwire 0.1.0 → 0.4.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/README.md CHANGED
@@ -14,11 +14,16 @@ bun install @seanmozeik/tripwire
14
14
  tripwire test '<command>' # Test a command
15
15
  tripwire test --tool=Read --path=.env # Test Read tool
16
16
  tripwire test --post --tool=Bash --stdout='ghp_TOKEN' # Test PostToolUse
17
+
18
+ tripwire install claude # Install hooks for Claude Code
19
+ tripwire install codex # Install hooks for Codex
20
+ tripwire install pi # Install hooks for pi-guardrails
21
+ tripwire install all # Install hooks for all agents
17
22
  ```
18
23
 
19
24
  ## Hook Configuration
20
25
 
21
- Configure your AI agent to call `tripwire-hook` for hook events:
26
+ Configure your AI agent to call `tripwire-hook` for hook events. You can do this manually, or use the `tripwire install` command to automatically configure hooks for supported agents:
22
27
 
23
28
  ### Claude Code
24
29
 
@@ -60,11 +65,7 @@ Create `~/.config/tripwire/config.json` to customize behavior:
60
65
  { "pattern": "dangerous-tool", "message": "Use safer-alternative instead", "action": "deny" }
61
66
  ],
62
67
  "allowedCommands": [
63
- {
64
- "pattern": "my-custom-tool",
65
- "message": "Allowing my-custom-tool per your configuration",
66
- "action": "allow"
67
- }
68
+ { "pattern": "my-custom-tool", "message": "Allowing my-custom-tool per your configuration" }
68
69
  ]
69
70
  }
70
71
  ```
@@ -95,6 +96,8 @@ Array of custom command blocks:
95
96
  - `pattern` (string) — Command pattern to block (uses shell parsing for matching)
96
97
  - `message` (string) — Error message shown when blocked
97
98
  - `action` (`"deny"` | `"ask"`, default: `"deny"`) — Whether to deny or ask for confirmation
99
+ - `requiresFlags` (string[], optional) — Match only when every listed flag is present, including `--flag=value` form
100
+ - `forbidsFlagValues` (array, optional) — Match only when each listed flag is present with one of the listed values
98
101
 
99
102
  #### `allowedCommands`
100
103
 
@@ -102,7 +105,8 @@ Array of custom command allows (overrides blocks):
102
105
 
103
106
  - `pattern` (string) — Command pattern to allow
104
107
  - `message` (string) — Message shown when allowed
105
- - `action` (string, default: `"allow"`) — Always `"allow"` for this context
108
+ - `requiresFlags` (string[], optional) — Same matching condition as `blockedCommands`
109
+ - `forbidsFlagValues` (array, optional) — Same matching condition as `blockedCommands`
106
110
 
107
111
  ### Shell-Based Command Matching
108
112
 
@@ -110,6 +114,9 @@ Command patterns in `blockedCommands` and `allowedCommands` use the same shell p
110
114
 
111
115
  - `rm` matches any `rm` command
112
116
  - `git push` matches `git push` with any arguments
117
+ - `gog calendar create` matches that head + subcommand path, not every `gog` command
118
+ - `requiresFlags: ["--attendees"]` matches `--attendees X` and `--attendees=X`
119
+ - `forbidsFlagValues: [{ "flag": "--send-updates", "values": ["all"] }]` matches `--send-updates all` and `--send-updates=all`
113
120
  - Patterns are parsed using shell-quote for accurate matching
114
121
  - More sophisticated than simple regex
115
122
 
@@ -122,6 +129,17 @@ Example:
122
129
  "pattern": "brew install",
123
130
  "message": "Use brew install with explicit version pinning",
124
131
  "action": "ask"
132
+ },
133
+ {
134
+ "pattern": "gog calendar create",
135
+ "requiresFlags": ["--attendees"],
136
+ "message": "Calendar invite sends email; draft it in chat first.",
137
+ "action": "deny"
138
+ },
139
+ {
140
+ "pattern": "gog calendar delete",
141
+ "forbidsFlagValues": [{ "flag": "--send-updates", "values": ["all", "externalOnly"] }],
142
+ "message": "Cancellation sends email; use --send-updates none or ask first."
125
143
  }
126
144
  ]
127
145
  }
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun @bytecode @bun-cjs
3
- (function(exports, require, module, __filename, __dirname) {var L=require("child_process"),M="/Users/sean/dev/tripwire/src/../dist/tripwire.js",O=(f)=>{let j="Bash",z=!1,x,F,G,J,K;for(let q of f){if(q==="--post"){z=!0;continue}if(q.startsWith("--tool=")){j=q.slice(7);continue}if(q.startsWith("--path=")){x=q.slice(7);continue}if(q.startsWith("--stdout=")){G=q.slice(9);continue}if(q.startsWith("--stderr=")){J=q.slice(9);continue}if(q.startsWith("--content=")){K=q.slice(10);continue}F??=q}return{tool:j,post:z,path:x,command:F,stdout:G,stderr:J,content:K}},Q=(f,j)=>{if(f==="Bash")return{command:j.command??""};if(f==="Read")return{file_path:j.path??""};if(f==="Write")return{file_path:j.path??"",content:j.content??""};if(f==="Edit"||f==="MultiEdit")return{file_path:j.path??"",old_string:"",new_string:j.content??""};return},R=(f)=>{let j=f.post?"PostToolUse":"PreToolUse",z=f.tool,x={hook_event_name:j,tool_name:z,cwd:process.cwd(),session_id:"tripwire-cli-test",tool_input:Q(z,f)};if(f.post)x.tool_response=z==="Bash"?{stdout:f.stdout??"",stderr:f.stderr??""}:{content:f.content??""};return x},V=()=>{process.stdout.write(["tripwire CLI \u2014 synthetic-event tester","","Usage:"," tripwire test '<command>' # PreToolUse Bash"," tripwire test --tool=Read --path=.env # PreToolUse Read"," tripwire test --tool=Write --path=foo.ts --content='TODO finish'"," tripwire test --post --tool=Bash --stdout='ghp_REAL_TOKEN' # PostToolUse",""].join(`
4
- `))},W=()=>{let f=process.argv.slice(2);if(f.length===0||f[0]!=="test")V(),process.exit(0);let j=O(f.slice(1)),z=R(j),x=L.spawnSync(M,[],{input:JSON.stringify(z),encoding:"utf8",timeout:1e4});if(x.error!==void 0)process.stderr.write(`error: ${String(x.error)}
5
- `),process.exit(1);let F=x.stdout;try{let G=JSON.parse(F);process.stdout.write(`${JSON.stringify(G,null,2)}
6
- `)}catch{process.stdout.write(F)}};W();})
3
+ (function(exports, require, module, __filename, __dirname) {var S=require("@effect/platform-bun"),Q=require("effect"),w=require("effect/unstable/cli");var W={name:"@seanmozeik/tripwire",version:"0.4.0",description:"Opinionated hooks dispatcher for AI coding agents with configurable safety rules",license:"MIT",bin:{tripwire:"./dist/tripwire-cli.js","tripwire-hook":"./dist/tripwire.js"},files:["dist","package.json","src","README.md"],type:"module",exports:{".":"./src/index.ts"},publishConfig:{access:"public"},scripts:{build:"bun scripts/build.ts",prepublishOnly:"bun run build",check:"bun run format && bun run lint:fix && bun run typecheck",format:"oxfmt --write .",lint:"oxlint --tsconfig tsconfig.oxlint.json .","lint:fix":"oxlint --format agent --tsconfig tsconfig.oxlint.json --fix .",test:"bun test",typecheck:"tsc --noEmit"},dependencies:{"@effect/platform-bun":"^4.0.0-beta.65",effect:"^4.0.0-beta.65","shell-quote":"^1.8.3"},devDependencies:{"@types/bun":"^1.3.13","@types/shell-quote":"^1.7.5",oxfmt:"^0.48.0",oxlint:"^1.61.0","oxlint-tsgolint":"^0.22.1",typescript:"^6.0.3"},engines:{bun:">=1.0"}};var B=require("os"),N=globalThis.Bun,X="tripwire-hook",Z=(b)=>{if(!b)return[[{hooks:[{type:"command",command:X}]}],!1];let q=!1,x=b.map((L)=>({hooks:L.hooks.map((D)=>{if(D.command===X||D.command.endsWith("/tripwire-hook")){if(D.command!==X)return q=!0,{...D,command:X};return D}return D})}));if(x.some((L)=>L.hooks.some((D)=>D.command===X)))return[x,!q];return[[...x,{hooks:[{type:"command",command:X}]}],!1]},v=async()=>{let b=`${B.homedir()}/.claude/settings.json`,q=N.file(b);try{let x=await q.text(),y=JSON.parse(x);y.hooks??={};let[G,L]=Z(y.hooks.PreToolUse),[D,j]=Z(y.hooks.PostToolUse);if(y.hooks.PreToolUse=G,y.hooks.PostToolUse=D,L&&j)return{success:!0,message:`Already configured: ${b}`};return await q.write(`${JSON.stringify(y,null,2)}
4
+ `),{success:!0,message:`Updated ${b}`}}catch(x){let y=x instanceof Error?x.message:String(x);if(y.includes("No such file"))return{success:!1,message:`Config file not found: ${b}`};return{success:!1,message:`Failed to update Claude config: ${y}`}}},A=async()=>{let b=`${B.homedir()}/.pi/agent/settings.json`,q=N.file(b);try{let x=await q.text(),y=JSON.parse(x);y.hooks??={};let[G,L]=Z(y.hooks.PreToolUse),[D,j]=Z(y.hooks.PostToolUse);if(y.hooks.PreToolUse=G,y.hooks.PostToolUse=D,L&&j)return{success:!0,message:`Already configured: ${b}`};return await q.write(`${JSON.stringify(y,null,2)}
5
+ `),{success:!0,message:`Updated ${b}`}}catch(x){let y=x instanceof Error?x.message:String(x);if(y.includes("No such file"))return{success:!1,message:`Config file not found: ${b}`};return{success:!1,message:`Failed to update pi config: ${y}`}}},J=async()=>{let b=`${B.homedir()}/.codex/config.toml`,q=`${B.homedir()}/.codex/hooks.json`,x=N.file(q),y=N.file(b),G=!1,L=!1;try{let D=await x.text(),j=JSON.parse(D);j.hooks??={};let[Y,V]=Z(j.hooks.PreToolUse),[z,$]=Z(j.hooks.PostToolUse);if(j.hooks.PreToolUse=Y,j.hooks.PostToolUse=z,!V||!$)G=!0;let K=(U)=>{return U?.map((E)=>({hooks:E.hooks.map((M)=>{if(M.command===X&&M.timeout===void 0)return{...M,timeout:10};return M})}))??[]};if(j.hooks.PreToolUse=K(j.hooks.PreToolUse),j.hooks.PostToolUse=K(j.hooks.PostToolUse),G)await x.write(`${JSON.stringify(j,null,2)}
6
+ `)}catch(D){let j=D instanceof Error?D.message:String(D);if(j.includes("No such file"))return{success:!1,message:`Config file not found: ${q}`};return{success:!1,message:`Failed to update Codex hooks.json: ${j}`}}try{let j=await y.text();if(j.includes("hooks = true"));else if(L=!0,j.includes("[features]")){let Y=j.indexOf("[features]"),V=j.indexOf(`
7
+ [`,Y+1);if(V===-1)j+=`
8
+ hooks = true`;else j=`${j.slice(0,V)}
9
+ hooks = true${j.slice(V)}`}else j+=`
10
+ [features]
11
+ hooks = true`;if(L)await y.write(j)}catch(D){let j=D instanceof Error?D.message:String(D);if(j.includes("No such file"))return{success:!1,message:`Config file not found: ${b}`};return{success:!1,message:`Failed to update Codex config.toml: ${j}`}}if(!G&&!L)return{success:!0,message:`Already configured: ${b} and ${q}`};return{success:!0,message:`Updated ${b} and ${q}`}},_=async()=>{return[{target:"claude",...await v()},{target:"codex",...await J()},{target:"pi",...await A()}]};var C="/Users/sean/dev/tripwire/src/../dist/tripwire.js",H=(b,q,x,y)=>{if(b==="Bash")return{command:q??""};if(b==="Read")return{file_path:x??""};if(b==="Write")return{file_path:x??"",content:y??""};if(b==="Edit"||b==="MultiEdit")return{file_path:x??"",old_string:"",new_string:y??""};return},O=(b)=>{let{tool:q,post:x,command:y,path:G,stdout:L,stderr:D,content:j}=b,V={hook_event_name:x?"PostToolUse":"PreToolUse",tool_name:q,cwd:process.cwd(),session_id:"tripwire-cli-test",tool_input:H(q,y,G,j)};if(x)V.tool_response=q==="Bash"?{stdout:L??"",stderr:D??""}:{content:j??""};return V},u=(b)=>Q.Effect.sync(()=>{let{command:q,content:x,path:y,post:G,stderr:L,stdout:D,tool:j}=b,Y=O({tool:j,post:G,command:q,path:y,stdout:D,stderr:L,content:x}),V=Bun.spawnSync([C],{stdin:new TextEncoder().encode(JSON.stringify(Y)),timeout:1e4,stdout:"pipe",stderr:"pipe"});if(V.exitCode!==0){let $=new TextDecoder().decode(V.stderr);console.error(`error: ${$}`),process.exit(1)}let z=new TextDecoder().decode(V.stdout);try{let $=JSON.parse(z);console.log(JSON.stringify($,null,2))}catch{console.log(z)}}),T=w.Command.make("test",{command:w.Argument.string("command").pipe(w.Argument.optional,w.Argument.withDescription("Command to test (for Bash tool)")),content:w.Flag.string("content").pipe(w.Flag.optional,w.Flag.withDescription("Content for Write/Edit tools")),path:w.Flag.string("path").pipe(w.Flag.optional,w.Flag.withDescription("File path for Read/Write/Edit tools")),post:w.Flag.boolean("post").pipe(w.Flag.withDescription("Test PostToolUse instead of PreToolUse")),stderr:w.Flag.string("stderr").pipe(w.Flag.optional,w.Flag.withDescription("Stderr for PostToolUse Bash")),stdout:w.Flag.string("stdout").pipe(w.Flag.optional,w.Flag.withDescription("Stdout for PostToolUse Bash")),tool:w.Flag.string("tool").pipe(w.Flag.withDefault("Bash"),w.Flag.withDescription("Tool name (Bash, Read, Write, Edit, MultiEdit)"))},({command:b,content:q,path:x,post:y,stderr:G,stdout:L,tool:D})=>u({command:Q.Option.getOrUndefined(b),content:Q.Option.getOrUndefined(q),path:Q.Option.getOrUndefined(x),post:y,stderr:Q.Option.getOrUndefined(G),stdout:Q.Option.getOrUndefined(L),tool:D})).pipe(w.Command.withDescription("Test a synthetic hook event")),I=(b)=>Q.Effect.gen(function*(){if(!["claude","codex","pi","all"].includes(b))console.error(`error: unknown target "${b}"`),console.error("Valid targets: claude, codex, pi, all"),process.exit(1);let q;switch(b){case"claude":{q=[{target:"claude",result:yield*Q.Effect.promise(()=>v())}];break}case"codex":{q=[{target:"codex",result:yield*Q.Effect.promise(()=>J())}];break}case"pi":{q=[{target:"pi",result:yield*Q.Effect.promise(()=>A())}];break}case"all":{q=(yield*Q.Effect.promise(()=>_())).map((G)=>({target:G.target,result:G}));break}default:{q=[];break}}let x=!1;for(let{target:y,result:G}of q)if(G.success){let L=G.message.startsWith("Already configured")?"\u2299":"\u2713";console.log(`${L} [${y}] ${G.message}`)}else console.error(`\u2717 [${y}] ${G.message}`),x=!0;if(x)process.exit(1)}),P=w.Command.make("install",{target:w.Argument.string("target").pipe(w.Argument.withDescription("Target agent (claude, codex, pi, or all)"))},({target:b})=>I(b)).pipe(w.Command.withDescription("Install tripwire hooks for AI agents")),F=w.Command.make("tripwire").pipe(w.Command.withDescription("Opinionated hooks dispatcher for AI coding agents"),w.Command.withSubcommands([T,P])),p=w.Command.run(F,{version:W.version}),c=async()=>{try{await Q.Effect.runPromise(p.pipe(Q.Effect.provide(S.BunServices.layer)))}catch(b){let q=b instanceof Error?b.message:String(b);console.error(q),process.exitCode=1}};c();})
Binary file