@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 +25 -7
- package/dist/tripwire-cli.js +9 -4
- package/dist/tripwire-cli.js.jsc +0 -0
- package/dist/tripwire.js +49 -49
- package/dist/tripwire.js.jsc +0 -0
- package/package.json +1 -1
- package/src/cli.ts +190 -98
- package/src/dispatch.ts +34 -10
- package/src/index.ts +2 -0
- package/src/lib/config.ts +6 -2
- package/src/lib/install.ts +238 -0
- package/src/rules/config-custom.ts +120 -11
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
|
-
- `
|
|
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
|
}
|
package/dist/tripwire-cli.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun @bytecode @bun-cjs
|
|
3
|
-
(function(exports, require, module, __filename, __dirname) {var
|
|
4
|
-
`)
|
|
5
|
-
`),
|
|
6
|
-
`)}catch{
|
|
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();})
|
package/dist/tripwire-cli.js.jsc
CHANGED
|
Binary file
|