@seanmozeik/tripwire 0.6.5 → 0.6.7
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 +117 -101
- package/dist/tripwire-cli.js +4 -4
- package/dist/tripwire-cli.js.jsc +0 -0
- package/dist/tripwire.js +69 -69
- package/dist/tripwire.js.jsc +0 -0
- package/package.json +11 -5
- package/src/rules/bash-tool-policy.ts +13 -1
package/README.md
CHANGED
|
@@ -1,33 +1,85 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tripwire
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](LICENSE) [](https://bun.sh)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A deterministic safety layer between an AI coding agent and your shell. Tripwire runs as a hook on every tool call, evaluates the command against a rule set, and blocks or rewrites the dangerous ones before they execute. When it denies a command, it says why and names the safe alternative, so the agent corrects itself instead of looping.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
$ tripwire test 'rm -rf /'
|
|
9
|
+
deny rm -rf on / is catastrophic and never intended.
|
|
10
|
+
|
|
11
|
+
$ tripwire test 'git push --force origin main'
|
|
12
|
+
deny Force-push to a protected branch (main) is blocked. Push to a feature branch and open a PR.
|
|
13
|
+
|
|
14
|
+
$ tripwire test 'curl https://get.example.sh | bash'
|
|
15
|
+
deny Piping a network script straight into a shell runs unreviewed code. Download it, read it, then run it.
|
|
9
16
|
```
|
|
10
17
|
|
|
11
|
-
##
|
|
18
|
+
## Why this exists
|
|
19
|
+
|
|
20
|
+
A coding agent is probabilistic. The damage it can do is not. A model that picks the right command 99% of the time will, given enough turns, eventually run `rm -rf` against the wrong directory, force-push over `main`, or paste a secret into a log. The cost of that one turn is not 1% of a good outcome. It is a wiped working tree or a leaked key.
|
|
21
|
+
|
|
22
|
+
The usual answer is a confirmation prompt: the agent proposes, a human approves. That breaks the moment the agent runs unattended, and it trains the human to click "yes" on everything anyway. Approval fatigue is not a safety model.
|
|
23
|
+
|
|
24
|
+
Tripwire takes a different line. Instead of asking a human to catch every dangerous command, it makes the worst classes of command unrepresentable at the shell boundary. The rules are deterministic code, not a model judging a model. `rm -rf /` is denied the same way every time, whether the agent is Claude Code, Codex, or something running headless at 3am.
|
|
25
|
+
|
|
26
|
+
The second idea matters as much as the first: every denial is written for the agent, not just logged. A rejection message names the rule and the safer path, so a capable agent reads it, adjusts, and moves on. The guardrail teaches rather than just stopping.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
Tripwire installs as a hook on your agent's tool lifecycle. It reads a tool-call event on stdin and returns a decision.
|
|
31
|
+
|
|
32
|
+
- **PreToolUse.** Before a command runs, every applicable rule votes. The most restrictive decision wins, so a single `deny` overrides any number of `allow`s. Decisions are `allow`, `deny`, `ask` (require confirmation), and `warn` (let it through, flag it).
|
|
33
|
+
- **PostToolUse.** After a command runs, tripwire scans the output and scrubs secrets before they reach the agent's context window.
|
|
34
|
+
|
|
35
|
+
Rules are pure, synchronous functions over the parsed command. Bash commands are tokenized with a real shell parser, not regex, so `git push` matches `git push` with any arguments while leaving `git push-mirror` alone, and a destructive `rm` buried inside a wrapper command is still seen for what it is.
|
|
36
|
+
|
|
37
|
+
## What it protects against
|
|
38
|
+
|
|
39
|
+
The defaults are opinionated but conservative. Nothing here blocks ordinary work.
|
|
40
|
+
|
|
41
|
+
**Catastrophic commands.** `rm -rf /`, fork bombs, `dd` to raw disks, and the handful of one-liners that have no safe use.
|
|
42
|
+
|
|
43
|
+
**Scoped destruction.** `rm` and `find -delete` are allowed only inside build and cache directories (`dist`, `build`, `node_modules`, `.next`, `/tmp`, and the rest). A delete anywhere else is denied with a pointer to `trash` or a graveyard tool, both recoverable.
|
|
44
|
+
|
|
45
|
+
**Git policy.** Read-only git is free. History rewriting (`rebase -i`, `filter-branch`, `commit --amend`), working-tree destruction (`reset --hard`, `clean -fd`, `checkout .`), force-push, and direct push to protected branches (`main`, `master`, `develop`, `production`, `release`) are blocked. Commits are required to use Conventional Commits format and an inline `-m` message.
|
|
46
|
+
|
|
47
|
+
**Network install scripts.** `curl … | bash` and `wget … | sh` are denied. Unreviewed code from the network does not get a shell.
|
|
48
|
+
|
|
49
|
+
**Tar bombs.** Extractions that would escape the target directory or overwrite outside it are caught before they unpack.
|
|
50
|
+
|
|
51
|
+
**Package-manager and tool policy.** Configurable nudges toward a single toolchain (for example bun over npm/pnpm/yarn) and toward modern equivalents of common utilities.
|
|
52
|
+
|
|
53
|
+
**File protection.** Reads and writes to `.env`, `.ssh/`, `*.pem`, `id_rsa*`, and similar are blocked so credentials never enter agent context.
|
|
54
|
+
|
|
55
|
+
**Secret scrubbing.** Tokens and keys in command output are redacted in the PostToolUse pass.
|
|
56
|
+
|
|
57
|
+
**Lazy-code warnings.** `TODO`, `FIXME`, and placeholder stubs in written code are flagged so half-finished work does not land silently.
|
|
58
|
+
|
|
59
|
+
Every default is configurable, and you can add your own allow and deny rules on top.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
12
62
|
|
|
13
63
|
```bash
|
|
14
|
-
|
|
15
|
-
tripwire test --tool=Read --path=.env # Test Read tool
|
|
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
|
|
64
|
+
bun install -g @seanmozeik/tripwire
|
|
22
65
|
```
|
|
23
66
|
|
|
24
|
-
|
|
67
|
+
This puts two binaries on your PATH: `tripwire` (the CLI) and `tripwire-hook` (the dispatcher your agent calls).
|
|
25
68
|
|
|
26
|
-
|
|
69
|
+
## Wiring it into an agent
|
|
27
70
|
|
|
28
|
-
|
|
71
|
+
Use the installer to configure hooks automatically:
|
|
29
72
|
|
|
30
|
-
|
|
73
|
+
```bash
|
|
74
|
+
tripwire install claude # Claude Code
|
|
75
|
+
tripwire install codex # Codex
|
|
76
|
+
tripwire install pi # pi-guardrails
|
|
77
|
+
tripwire install all # every supported agent
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
To wire it by hand, point the agent's hook events at `tripwire-hook`.
|
|
81
|
+
|
|
82
|
+
**Claude Code** (`~/.claude/settings.json`):
|
|
31
83
|
|
|
32
84
|
```jsonc
|
|
33
85
|
{
|
|
@@ -38,21 +90,26 @@ Configure your AI agent to call `tripwire-hook` for hook events. You can do this
|
|
|
38
90
|
}
|
|
39
91
|
```
|
|
40
92
|
|
|
41
|
-
|
|
93
|
+
**Codex** uses the same hook format as Claude Code.
|
|
42
94
|
|
|
43
|
-
|
|
95
|
+
**Devin** and other agents: configure the agent to call `tripwire-hook` on tool events.
|
|
44
96
|
|
|
45
|
-
|
|
97
|
+
## Testing rules
|
|
46
98
|
|
|
47
|
-
|
|
99
|
+
`tripwire test` evaluates a command without running it, so you can check what a rule does before trusting it in a live loop.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
tripwire test 'rm -rf /' # a bash command
|
|
103
|
+
tripwire test --tool=Read --path=.env # a file read
|
|
104
|
+
tripwire test --post --tool=Bash --stdout='ghp_TOKEN' # a PostToolUse output scan
|
|
105
|
+
```
|
|
48
106
|
|
|
49
107
|
## Configuration
|
|
50
108
|
|
|
51
|
-
|
|
109
|
+
Drop a `~/.config/tripwire/config.json` to extend or adjust the defaults. Unknown keys are rejected loudly rather than ignored, so a typo fails fast instead of silently disabling a rule.
|
|
52
110
|
|
|
53
111
|
```json
|
|
54
112
|
{
|
|
55
|
-
"rtk": { "enabled": true, "path": "/opt/homebrew/bin/rtk" },
|
|
56
113
|
"git": {
|
|
57
114
|
"protectedBranches": ["main", "master", "develop", "production", "release"],
|
|
58
115
|
"enforceConventionalCommits": true
|
|
@@ -70,64 +127,48 @@ Create `~/.config/tripwire/config.json` to customize behavior:
|
|
|
70
127
|
}
|
|
71
128
|
```
|
|
72
129
|
|
|
73
|
-
###
|
|
74
|
-
|
|
75
|
-
#### `rtk`
|
|
76
|
-
|
|
77
|
-
- `enabled` (boolean, default: `false`) — Enable rtk token-saver integration
|
|
78
|
-
- `path` (string, optional) — Path to rtk binary. If not specified, searches common locations.
|
|
79
|
-
|
|
80
|
-
#### `git`
|
|
81
|
-
|
|
82
|
-
- `protectedBranches` (string[], default: `["main", "master", "develop", "production", "release"]`) — Branches that require PR for push
|
|
83
|
-
- `enforceConventionalCommits` (boolean, default: `true`) — Enforce Conventional Commits format for commit messages
|
|
130
|
+
### Options
|
|
84
131
|
|
|
85
|
-
|
|
132
|
+
**`git`**
|
|
86
133
|
|
|
87
|
-
- `
|
|
88
|
-
- `
|
|
134
|
+
- `protectedBranches` (string[], default `["main", "master", "develop", "production", "release"]`): branches that cannot be pushed to directly.
|
|
135
|
+
- `enforceConventionalCommits` (boolean, default `true`): require Conventional Commits format for commit messages.
|
|
89
136
|
|
|
90
|
-
|
|
137
|
+
**`safePaths`**
|
|
91
138
|
|
|
92
|
-
|
|
139
|
+
- `relative` (string[]): additional relative paths where destructive operations are allowed.
|
|
140
|
+
- `absolute` (string[]): additional absolute paths where destructive operations are allowed.
|
|
93
141
|
|
|
94
|
-
|
|
142
|
+
Built-in safe paths already cover `dist`, `build`, `.next`, `node_modules`, `/tmp`, `/var/tmp`, and other common build and cache directories.
|
|
95
143
|
|
|
96
|
-
|
|
97
|
-
- `message` (string) — Error message shown when blocked
|
|
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
|
|
144
|
+
**`blockedCommands`** is an array of custom denials:
|
|
101
145
|
|
|
102
|
-
|
|
146
|
+
- `pattern` (string): the command to match, parsed as shell tokens.
|
|
147
|
+
- `message` (string): what the agent sees when blocked.
|
|
148
|
+
- `action` (`"deny"` | `"ask"`, default `"deny"`): deny outright or require confirmation.
|
|
149
|
+
- `requiresFlags` (string[]): match only when every listed flag is present, including `--flag=value` form.
|
|
150
|
+
- `forbidsFlagValues` (array): match only when each listed flag carries one of the listed values.
|
|
103
151
|
|
|
104
|
-
|
|
152
|
+
**`allowedCommands`** is an array of custom allows that override blocks. Same fields as `blockedCommands`.
|
|
105
153
|
|
|
106
|
-
|
|
107
|
-
- `message` (string) — Message shown when allowed
|
|
108
|
-
- `requiresFlags` (string[], optional) — Same matching condition as `blockedCommands`
|
|
109
|
-
- `forbidsFlagValues` (array, optional) — Same matching condition as `blockedCommands`
|
|
154
|
+
### How command matching works
|
|
110
155
|
|
|
111
|
-
|
|
156
|
+
Patterns are parsed with the same shell tokenizer as the rest of tripwire, so matching is structural rather than substring.
|
|
112
157
|
|
|
113
|
-
|
|
158
|
+
- `rm` matches any `rm` invocation.
|
|
159
|
+
- `git push` matches `git push` with any arguments.
|
|
160
|
+
- `gog calendar create` matches that head plus subcommand path, not every `gog` command.
|
|
161
|
+
- `requiresFlags: ["--attendees"]` matches `--attendees X` and `--attendees=X`.
|
|
162
|
+
- `forbidsFlagValues: [{ "flag": "--send-updates", "values": ["all"] }]` matches `--send-updates all` and `--send-updates=all`.
|
|
114
163
|
|
|
115
|
-
|
|
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`
|
|
120
|
-
- Patterns are parsed using shell-quote for accurate matching
|
|
121
|
-
- More sophisticated than simple regex
|
|
122
|
-
|
|
123
|
-
Example:
|
|
164
|
+
A worked example, blocking calendar invites that would send email until a human has reviewed them:
|
|
124
165
|
|
|
125
166
|
```json
|
|
126
167
|
{
|
|
127
168
|
"blockedCommands": [
|
|
128
169
|
{
|
|
129
170
|
"pattern": "brew install",
|
|
130
|
-
"message": "
|
|
171
|
+
"message": "Pin an explicit version when installing.",
|
|
131
172
|
"action": "ask"
|
|
132
173
|
},
|
|
133
174
|
{
|
|
@@ -145,45 +186,20 @@ Example:
|
|
|
145
186
|
}
|
|
146
187
|
```
|
|
147
188
|
|
|
148
|
-
##
|
|
149
|
-
|
|
150
|
-
Tripwire comes with opinionated but reasonable defaults:
|
|
151
|
-
|
|
152
|
-
### Bash Safety
|
|
153
|
-
|
|
154
|
-
- Blocks catastrophic commands: `rm -rf /`, fork bombs, `dd` to disks
|
|
155
|
-
- Blocks macOS system mutations: `defaults write`, `launchctl`, `diskutil erase`
|
|
156
|
-
- Blocks cloud destructive operations: `gh repo delete`, `flyctl destroy`
|
|
157
|
-
- Scopes `rm` and `find -delete` to safe paths (build outputs, cache directories)
|
|
158
|
-
- Blocks network install scripts: `curl | bash`, `wget | sh`
|
|
159
|
-
- Enforces package manager policy: Bun-only, no npm/pnpm/yarn/pip
|
|
160
|
-
|
|
161
|
-
### Git Policy
|
|
162
|
-
|
|
163
|
-
- Read-only operations allowed: `status`, `log`, `diff`, `fetch`, etc.
|
|
164
|
-
- Blocks working-tree destruction: `reset --hard`, `clean -fd`, `checkout .`
|
|
165
|
-
- Blocks history rewriting: `rebase -i`, `filter-branch`, `commit --amend`
|
|
166
|
-
- Blocks force push and protected branch pushes
|
|
167
|
-
- Enforces Conventional Commits format (configurable)
|
|
168
|
-
- Requires `-m "message"` for commits (no editor mode)
|
|
169
|
-
- Asks for confirmation on merge/rebase/cherry-pick
|
|
189
|
+
## Bypassing a rule
|
|
170
190
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
- Blocks reads/writes to `.env`, `.ssh/`, `*.pem`, `id_rsa*`, etc.
|
|
174
|
-
- Warns on TODO/FIXME/placeholder in code (configurable)
|
|
175
|
-
- Scrubs secrets from tool output
|
|
176
|
-
|
|
177
|
-
## Bypass
|
|
178
|
-
|
|
179
|
-
Add `# tripwire-allow: <reason>` to bypass any rule:
|
|
191
|
+
When a blocked command is genuinely what you want, append a reason and tripwire lets it through:
|
|
180
192
|
|
|
181
193
|
```bash
|
|
182
|
-
rm -rf /tmp/test
|
|
183
|
-
git reset --hard HEAD~1
|
|
194
|
+
rm -rf /tmp/test # tripwire-allow: cleaning a test directory
|
|
195
|
+
git reset --hard HEAD~1 # tripwire-allow: undoing a mistaken commit
|
|
184
196
|
```
|
|
185
197
|
|
|
186
|
-
|
|
198
|
+
The reason is required, which keeps the bypass deliberate and leaves a trail in the command itself.
|
|
199
|
+
|
|
200
|
+
## Library usage
|
|
201
|
+
|
|
202
|
+
The decision primitives are exported for building custom rules or embedding tripwire elsewhere:
|
|
187
203
|
|
|
188
204
|
```typescript
|
|
189
205
|
import { allow, deny, ask, warn } from '@seanmozeik/tripwire';
|
|
@@ -194,11 +210,11 @@ import type { Decision, Config } from '@seanmozeik/tripwire';
|
|
|
194
210
|
|
|
195
211
|
```bash
|
|
196
212
|
bun install
|
|
197
|
-
bun run build #
|
|
198
|
-
bun run check #
|
|
199
|
-
bun test
|
|
213
|
+
bun run build # build dist/tripwire.js and dist/tripwire-cli.js
|
|
214
|
+
bun run check # format, lint, typecheck
|
|
215
|
+
bun test
|
|
200
216
|
```
|
|
201
217
|
|
|
202
218
|
## License
|
|
203
219
|
|
|
204
|
-
MIT
|
|
220
|
+
MIT.
|
package/dist/tripwire-cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun @bytecode @bun-cjs
|
|
3
|
-
(function(exports, require, module, __filename, __dirname) {var S=require("path"),U=require("@effect/platform-bun"),C=globalThis.Bun,Q=require("effect"),
|
|
4
|
-
`),{success:!0,message:`Updated ${b}`}}catch(
|
|
5
|
-
`),{success:!0,message:`Updated ${b}`}}catch(
|
|
3
|
+
(function(exports, require, module, __filename, __dirname) {var S=require("path"),U=require("@effect/platform-bun"),C=globalThis.Bun,Q=require("effect"),q=require("effect/unstable/cli");var W={name:"@seanmozeik/tripwire",version:"0.6.7",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 .",prepare:"bunx effect-tsgo patch",test:"bun test",typecheck:"tsc --noEmit"},dependencies:{"shell-quote":"^1.8.4"},devDependencies:{"@effect/platform-bun":"^4.0.0-beta.102","@effect/tsgo":"^0.24.3","@types/bun":"^1.3.14","@types/shell-quote":"^1.7.5",effect:"^4.0.0-beta.102",oxfmt:"^0.53.0",oxlint:"1.68.0","oxlint-tsgolint":"^0.23.0",typescript:"^7.0.2"},peerDependencies:{"@effect/platform-bun":"^4.0.0-beta.102",effect:"^4.0.0-beta.102"},engines:{bun:">=1.0"}};var N=require("os"),z=globalThis.Bun,X="tripwire-hook",$=(b)=>{if(!b)return[[{hooks:[{type:"command",command:X}]}],!1];let w=!1,x=b.map((L)=>({hooks:L.hooks.map((D)=>{if(D.command===X||D.command.endsWith("/tripwire-hook")){if(D.command!==X)return w=!0,{...D,command:X};return D}return D})}));if(x.some((L)=>L.hooks.some((D)=>D.command===X)))return[x,!w];return[[...x,{hooks:[{type:"command",command:X}]}],!1]},v=async()=>{let b=`${N.homedir()}/.claude/settings.json`,w=z.file(b);try{let x=await w.text(),y=JSON.parse(x);y.hooks??={};let[G,L]=$(y.hooks.PreToolUse),[D,j]=$(y.hooks.PostToolUse);if(y.hooks.PreToolUse=G,y.hooks.PostToolUse=D,L&&j)return{success:!0,message:`Already configured: ${b}`};return await w.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}`}}},J=async()=>{let b=`${N.homedir()}/.pi/agent/settings.json`,w=z.file(b);try{let x=await w.text(),y=JSON.parse(x);y.hooks??={};let[G,L]=$(y.hooks.PreToolUse),[D,j]=$(y.hooks.PostToolUse);if(y.hooks.PreToolUse=G,y.hooks.PostToolUse=D,L&&j)return{success:!0,message:`Already configured: ${b}`};return await w.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}`}}},K=async()=>{let b=`${N.homedir()}/.codex/config.toml`,w=`${N.homedir()}/.codex/hooks.json`,x=z.file(w),y=z.file(b),G=!1,L=!1;try{let D=await x.text(),j=JSON.parse(D);j.hooks??={};let[Y,V]=$(j.hooks.PreToolUse),[B,A]=$(j.hooks.PostToolUse);if(j.hooks.PreToolUse=Y,j.hooks.PostToolUse=B,!V||!A)G=!0;let Z=(R)=>{return R?.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=Z(j.hooks.PreToolUse),j.hooks.PostToolUse=Z(j.hooks.PostToolUse),G)await x.write(`${JSON.stringify(j,null,2)}
|
|
6
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: ${w}`};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
7
|
[`,Y+1);if(V===-1)j+=`
|
|
8
8
|
hooks = true`;else j=`${j.slice(0,V)}
|
|
9
9
|
hooks = true${j.slice(V)}`}else j+=`
|
|
10
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 ${w}`};return{success:!0,message:`Updated ${b} and ${w}`}},_=async()=>{return[{target:"claude",...await v()},{target:"codex",...await K()},{target:"pi",...await J()}]};var O=()=>{return/\/bun(?<ext>\.exe)?$/.test(process.argv[0]??"")?process.argv[1]:process.argv[0]},u=async()=>{let b=O(),w=S.dirname(b),
|
|
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 ${w}`};return{success:!0,message:`Updated ${b} and ${w}`}},_=async()=>{return[{target:"claude",...await v()},{target:"codex",...await K()},{target:"pi",...await J()}]};var O=()=>{return/\/bun(?<ext>\.exe)?$/.test(process.argv[0]??"")?process.argv[1]:process.argv[0]},u=async()=>{let b=O(),w=S.dirname(b),x=`${w}/tripwire-hook`;try{return await C.file(x).text(),x}catch{return`${w}/tripwire.js`}},T=(b,w,x,y)=>{if(b==="Bash")return{command:w??""};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},I=(b)=>{let{tool:w,post:x,command:y,path:G,stdout:L,stderr:D,content:j}=b,V={hook_event_name:x?"PostToolUse":"PreToolUse",tool_name:w,cwd:process.cwd(),session_id:"tripwire-cli-test",tool_input:T(w,y,G,j)};if(x)V.tool_response=w==="Bash"?{stdout:L??"",stderr:D??""}:{content:j??""};return V},F=(b)=>Q.Effect.gen(function*(){let{command:w,content:x,path:y,post:G,stderr:L,stdout:D,tool:j}=b,Y=I({tool:j,post:G,command:w,path:y,stdout:D,stderr:L,content:x}),V=yield*Q.Effect.promise(()=>u()),B=Bun.spawnSync([V],{stdin:new TextEncoder().encode(JSON.stringify(Y)),timeout:1e4,stdout:"pipe",stderr:"pipe"});if(B.exitCode!==0){let Z=new TextDecoder().decode(B.stderr);console.error(`error: ${Z}`),process.exit(1)}let A=new TextDecoder().decode(B.stdout);try{let Z=JSON.parse(A);console.log(JSON.stringify(Z,null,2))}catch{console.log(A)}}),P=q.Command.make("test",{command:q.Argument.string("command").pipe(q.Argument.optional,q.Argument.withDescription("Command to test (for Bash tool)")),content:q.Flag.string("content").pipe(q.Flag.optional,q.Flag.withDescription("Content for Write/Edit tools")),path:q.Flag.string("path").pipe(q.Flag.optional,q.Flag.withDescription("File path for Read/Write/Edit tools")),post:q.Flag.boolean("post").pipe(q.Flag.withDescription("Test PostToolUse instead of PreToolUse")),stderr:q.Flag.string("stderr").pipe(q.Flag.optional,q.Flag.withDescription("Stderr for PostToolUse Bash")),stdout:q.Flag.string("stdout").pipe(q.Flag.optional,q.Flag.withDescription("Stdout for PostToolUse Bash")),tool:q.Flag.string("tool").pipe(q.Flag.withDefault("Bash"),q.Flag.withDescription("Tool name (Bash, Read, Write, Edit, MultiEdit)"))},({command:b,content:w,path:x,post:y,stderr:G,stdout:L,tool:D})=>F({command:Q.Option.getOrUndefined(b),content:Q.Option.getOrUndefined(w),path:Q.Option.getOrUndefined(x),post:y,stderr:Q.Option.getOrUndefined(G),stdout:Q.Option.getOrUndefined(L),tool:D})).pipe(q.Command.withDescription("Test a synthetic hook event")),p=(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 w;switch(b){case"claude":{w=[{target:"claude",result:yield*Q.Effect.promise(()=>v())}];break}case"codex":{w=[{target:"codex",result:yield*Q.Effect.promise(()=>K())}];break}case"pi":{w=[{target:"pi",result:yield*Q.Effect.promise(()=>J())}];break}case"all":{w=(yield*Q.Effect.promise(()=>_())).map((G)=>({target:G.target,result:G}));break}default:{w=[];break}}let x=!1;for(let{target:y,result:G}of w)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)}),c=q.Command.make("install",{target:q.Argument.string("target").pipe(q.Argument.withDescription("Target agent (claude, codex, pi, or all)"))},({target:b})=>p(b)).pipe(q.Command.withDescription("Install tripwire hooks for AI agents")),k=q.Command.make("tripwire").pipe(q.Command.withDescription("Opinionated hooks dispatcher for AI coding agents"),q.Command.withSubcommands([P,c])),f=q.Command.run(k,{version:W.version}),h=async()=>{try{await Q.Effect.runPromise(f.pipe(Q.Effect.provide(U.BunServices.layer)))}catch(b){let w=b instanceof Error?b.message:String(b);console.error(w),process.exitCode=1}};h();})
|
package/dist/tripwire-cli.js.jsc
CHANGED
|
Binary file
|