@seanmozeik/tripwire 0.6.7 → 0.7.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sean Mozeik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,220 +1,231 @@
1
1
  # Tripwire
2
2
 
3
- [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![runtime: bun](https://img.shields.io/badge/runtime-bun-f9f1e1.svg)](https://bun.sh)
3
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![runtime: Bun 1.4](https://img.shields.io/badge/runtime-Bun%201.4-f9f1e1.svg)](https://bun.sh)
4
4
 
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.
5
+ Tripwire is a configurable hook dispatcher for coding agents. It checks shell and file tool calls before execution. It also scans selected tool output for secrets after execution.
6
6
 
7
- ```bash
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.
7
+ ```text
8
+ $ tripwire test 'git status'
9
+ {
10
+ "continue": true
11
+ }
16
12
  ```
17
13
 
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.
14
+ ## Package support
40
15
 
41
- **Catastrophic commands.** `rm -rf /`, fork bombs, `dd` to raw disks, and the handful of one-liners that have no safe use.
16
+ The published package contains a Bun 1.4 bytecode executable for Darwin arm64. Its package metadata rejects registry installation on Linux, Windows, and Intel macOS.
42
17
 
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.
18
+ ```bash
19
+ bun install --global @seanmozeik/tripwire
20
+ ```
46
21
 
47
- **Network install scripts.** `curl … | bash` and `wget … | sh` are denied. Unreviewed code from the network does not get a shell.
22
+ The package installs two commands:
48
23
 
49
- **Tar bombs.** Extractions that would escape the target directory or overwrite outside it are caught before they unpack.
24
+ - `tripwire-hook` points directly to the native executable. Agent hooks use this path.
25
+ - `tripwire` is a small POSIX launcher for interactive CLI commands. It executes the same native file in CLI mode.
50
26
 
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.
27
+ Other platforms must build from source on the target host:
52
28
 
53
- **File protection.** Reads and writes to `.env`, `.ssh/`, `*.pem`, `id_rsa*`, and similar are blocked so credentials never enter agent context.
29
+ ```bash
30
+ git clone https://github.com/seanmozeik/tripwire.git
31
+ cd tripwire
32
+ bun install --frozen-lockfile
33
+ bun run build
34
+ ./dist/tripwire --version
35
+ ```
54
36
 
55
- **Secret scrubbing.** Tokens and keys in command output are redacted in the PostToolUse pass.
37
+ The source build uses the current host. A later release can add target-specific registry packages for other systems.
56
38
 
57
- **Lazy-code warnings.** `TODO`, `FIXME`, and placeholder stubs in written code are flagged so half-finished work does not land silently.
39
+ ## Secret scanner requirement
58
40
 
59
- Every default is configurable, and you can add your own allow and deny rules on top.
41
+ Post-tool scanning requires Betterleaks 1.5.0 or later. Install `betterleaks` on `PATH` before you enable a post-tool hook. The default command is `betterleaks`. You can set another executable path in personal config.
60
42
 
61
- ## Install
43
+ Tripwire runs this command without a shell or temporary file:
62
44
 
63
- ```bash
64
- bun install -g @seanmozeik/tripwire
45
+ ```text
46
+ betterleaks stdin --report-format json --report-path -
65
47
  ```
66
48
 
67
- This puts two binaries on your PATH: `tripwire` (the CLI) and `tripwire-hook` (the dispatcher your agent calls).
49
+ Scanner failures are closed failures. If the executable is missing, times out, exits with an error, or returns malformed JSON, Tripwire sends a post-tool denial to hosts that support one. The error does not include scanned text, secret values, or raw scanner stderr.
68
50
 
69
- ## Wiring it into an agent
51
+ ## Install agent hooks
70
52
 
71
- Use the installer to configure hooks automatically:
53
+ Run one installer after the package and Betterleaks are available:
72
54
 
73
55
  ```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`):
83
-
84
- ```jsonc
85
- {
86
- "hooks": {
87
- "PreToolUse": [{ "hooks": [{ "type": "command", "command": "/path/to/tripwire-hook" }] }],
88
- "PostToolUse": [{ "hooks": [{ "type": "command", "command": "/path/to/tripwire-hook" }] }],
89
- },
90
- }
56
+ tripwire install claude
57
+ tripwire install codex
58
+ tripwire install cursor
59
+ tripwire install pi
60
+ tripwire install oh-my-pi
61
+ tripwire install all
91
62
  ```
92
63
 
93
- **Codex** uses the same hook format as Claude Code.
64
+ The installers update these paths:
94
65
 
95
- **Devin** and other agents: configure the agent to call `tripwire-hook` on tool events.
66
+ | Host | Files |
67
+ | ------------ | ----------------------------------------------------------------- |
68
+ | Claude Code | `~/.claude/settings.json` |
69
+ | Codex | `~/.codex/hooks.json`, `~/.codex/config.toml` |
70
+ | Cursor Agent | `~/.cursor/hooks.json` |
71
+ | Pi | `~/.pi/agent/settings.json`, `~/.pi/agent/extensions/tripwire.js` |
72
+ | Oh My Pi | `~/.omp/agent/extensions/tripwire.js` |
96
73
 
97
- ## Testing rules
74
+ Settings updates use same-directory atomic replacement. Existing file modes, unknown JSON fields, and unrelated Codex TOML bytes remain unchanged. Existing config symlinks remain symlinks.
98
75
 
99
- `tripwire test` evaluates a command without running it, so you can check what a rule does before trusting it in a live loop.
76
+ Pi and Oh My Pi use the native extension API. Their extension paths point to the packaged `tripwire-pi.js` adapter. Pi removes old Claude-style Tripwire hooks from its settings after the extension link is available. The adapter sends one batch to Tripwire for a multi-file edit.
100
77
 
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
- ```
78
+ ### Manual hook command
106
79
 
107
- ## Configuration
80
+ Claude Code and Codex use `tripwire-hook` for `PreToolUse` and `PostToolUse`. Codex also requires `hooks = true` in the `[features]` table of `~/.codex/config.toml`.
108
81
 
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.
82
+ Cursor uses event-specific commands because some payloads do not include the configured event name:
110
83
 
111
84
  ```json
112
85
  {
113
- "git": {
114
- "protectedBranches": ["main", "master", "develop", "production", "release"],
115
- "enforceConventionalCommits": true
116
- },
117
- "safePaths": {
118
- "relative": ["dist", "build", ".next", "node_modules"],
119
- "absolute": ["/tmp", "/var/tmp"]
120
- },
121
- "blockedCommands": [
122
- { "pattern": "dangerous-tool", "message": "Use safer-alternative instead", "action": "deny" }
123
- ],
124
- "allowedCommands": [
125
- { "pattern": "my-custom-tool", "message": "Allowing my-custom-tool per your configuration" }
126
- ]
86
+ "version": 1,
87
+ "hooks": {
88
+ "preToolUse": [{ "command": "tripwire-hook --cursor-event preToolUse", "failClosed": true }],
89
+ "postToolUse": [{ "command": "tripwire-hook --cursor-event postToolUse" }],
90
+ "beforeShellExecution": [
91
+ { "command": "tripwire-hook --cursor-event beforeShellExecution", "failClosed": true }
92
+ ],
93
+ "afterShellExecution": [{ "command": "tripwire-hook --cursor-event afterShellExecution" }],
94
+ "beforeReadFile": [
95
+ { "command": "tripwire-hook --cursor-event beforeReadFile", "failClosed": true }
96
+ ],
97
+ "afterFileEdit": [{ "command": "tripwire-hook --cursor-event afterFileEdit" }]
98
+ }
127
99
  }
128
100
  ```
129
101
 
130
- ### Options
102
+ ## Decisions and failure policy
103
+
104
+ Each applicable rule returns `allow`, `warn`, `ask`, or `deny`. The most restrictive result wins.
131
105
 
132
- **`git`**
106
+ Production hook evaluation isolates each rule. If one rule throws, Tripwire logs the defect, treats that rule as `allow`, and continues with later rules. The synchronous library API has the same throw isolation. Effect timeouts do not interrupt synchronous CPU work.
133
107
 
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.
108
+ Failure behavior depends on the stage and host:
136
109
 
137
- **`safePaths`**
110
+ - A missing personal config uses defaults. Invalid JSON, unknown keys, permission errors, and other read errors deny the next pre-tool call.
111
+ - Post-tool scanning still runs when personal config is invalid. It uses the default scanner settings for that call.
112
+ - A malformed native single-event hook input returns the host allow response. A malformed private batch is denied.
113
+ - Invalid Cursor pre-tool input is denied. Cursor post-tool output cannot be replaced after execution, so Tripwire returns the host allow response.
114
+ - Pi and Oh My Pi deny a tool call when the dispatcher fails. A post-tool denial or dispatcher failure stops the session.
138
115
 
139
- - `relative` (string[]): additional relative paths where destructive operations are allowed.
140
- - `absolute` (string[]): additional absolute paths where destructive operations are allowed.
116
+ PowerShell pre-tool calls are denied because Tripwire does not parse PowerShell grammar. PowerShell post-tool output is still scanned.
141
117
 
142
- Built-in safe paths already cover `dist`, `build`, `.next`, `node_modules`, `/tmp`, `/var/tmp`, and other common build and cache directories.
118
+ Internal errors are written to `~/.claude/tripwire.log`. Logging failure does not change the hook response.
143
119
 
144
- **`blockedCommands`** is an array of custom denials:
120
+ ## Built-in checks
145
121
 
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.
122
+ Tripwire includes checks for these operations:
151
123
 
152
- **`allowedCommands`** is an array of custom allows that override blocks. Same fields as `blockedCommands`.
124
+ - catastrophic shell commands such as `rm -rf /`, fork bombs, and raw-disk writes
125
+ - destructive Git commands and optional protected-branch policy
126
+ - network scripts piped into a shell
127
+ - deletion outside configured build, cache, and temporary paths
128
+ - protected files such as `.env`, SSH keys, private keys, and cloud credentials
129
+ - redirects, copies, and moves that target protected paths through symlink aliases
130
+ - optional package-manager and utility preferences from personal config
131
+ - new `TODO`, `FIXME`, fallback, and placeholder markers in code edits
153
132
 
154
- ### How command matching works
133
+ The archive check is narrow. It denies `tar` extraction with `x` or `--extract` when `-C` or `--directory` targets `/` or the home directory. It applies the same destination rule to `unzip -d`. Archive listing, including `tar -tf archive.tar -C /`, is allowed. Tripwire does not inspect archive member paths.
155
134
 
156
- Patterns are parsed with the same shell tokenizer as the rest of tripwire, so matching is structural rather than substring.
135
+ Compound Bash forms are inspected conservatively. Tripwire follows executable commands after `if`, `elif`, `then`, `else`, `while`, `until`, and `do`. It denies unsupported structures when it cannot identify every executable branch.
157
136
 
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`.
137
+ Protected-path checks compare the submitted path and its resolved target. New writes resolve the deepest existing parent, which prevents a symlink alias from hiding a protected destination.
163
138
 
164
- A worked example, blocking calendar invites that would send email until a human has reviewed them:
139
+ ## Personal config
140
+
141
+ Personal workflow preferences belong in `~/.config/tripwire/config.json`. A missing file uses open-source defaults. A present file with unknown keys or invalid values fails loudly.
165
142
 
166
143
  ```json
167
144
  {
145
+ "git": { "protectedBranches": ["main", "production"], "enforceConventionalCommits": true },
146
+ "safePaths": { "relative": ["dist", "build", "node_modules"], "absolute": ["/tmp", "/var/tmp"] },
147
+ "toolPolicies": [
148
+ {
149
+ "rule": "project-package-manager",
150
+ "executables": ["npm", "pnpm", "yarn"],
151
+ "action": "deny",
152
+ "message": "Use the package manager selected by this workspace."
153
+ }
154
+ ],
168
155
  "blockedCommands": [
169
156
  {
170
157
  "pattern": "brew install",
171
- "message": "Pin an explicit version when installing.",
158
+ "message": "Pin an explicit version before installation.",
172
159
  "action": "ask"
173
- },
174
- {
175
- "pattern": "gog calendar create",
176
- "requiresFlags": ["--attendees"],
177
- "message": "Calendar invite sends email; draft it in chat first.",
178
- "action": "deny"
179
- },
180
- {
181
- "pattern": "gog calendar delete",
182
- "forbidsFlagValues": [{ "flag": "--send-updates", "values": ["all", "externalOnly"] }],
183
- "message": "Cancellation sends email; use --send-updates none or ask first."
184
160
  }
185
- ]
161
+ ],
162
+ "allowedCommands": [
163
+ { "pattern": "project-safe-tool", "message": "This command is allowed by personal config." }
164
+ ],
165
+ "secretScanner": { "executable": "betterleaks", "timeoutMs": 5000 }
186
166
  }
187
167
  ```
188
168
 
189
- ## Bypassing a rule
169
+ `toolPolicies` accepts these optional match fields:
190
170
 
191
- When a blocked command is genuinely what you want, append a reason and tripwire lets it through:
171
+ - `argumentsIncludeAll`
172
+ - `argumentsStartWith`
173
+ - `shortFlagsIncludeAll`
174
+
175
+ Custom blocked commands can use `requiresFlags` and `forbidsFlagValues`. Patterns are parsed as shell tokens, so `git push` matches that command path and does not match `git push-mirror`.
176
+
177
+ ## Rule bypass
178
+
179
+ A bypass requires a shell comment, a colon, and a non-empty reason:
180
+
181
+ ```bash
182
+ git reset --hard HEAD~1 # tripwire-allow: discard the local experiment after review
183
+ ```
184
+
185
+ These forms do not bypass a rule:
192
186
 
193
187
  ```bash
194
- rm -rf /tmp/test # tripwire-allow: cleaning a test directory
195
- git reset --hard HEAD~1 # tripwire-allow: undoing a mistaken commit
188
+ git reset --hard HEAD~1 # tripwire-allow
189
+ git reset --hard HEAD~1 # tripwire-allow:
196
190
  ```
197
191
 
198
- The reason is required, which keeps the bypass deliberate and leaves a trail in the command itself.
192
+ The lazy-code rule accepts the same marker in an edited line. Catastrophic rules remain denied when a bypass reason is present.
199
193
 
200
- ## Library usage
194
+ ## Test a rule
201
195
 
202
- The decision primitives are exported for building custom rules or embedding tripwire elsewhere:
196
+ `tripwire test` creates a hook event and does not run the command:
197
+
198
+ ```bash
199
+ tripwire test 'rm -rf /'
200
+ tripwire test --tool=Read --path=.env
201
+ tripwire test --post --tool=Bash --stdout='example output'
202
+ ```
203
+
204
+ The post-tool example requires Betterleaks.
205
+
206
+ ## Library API
203
207
 
204
208
  ```typescript
205
- import { allow, deny, ask, warn } from '@seanmozeik/tripwire';
206
- import type { Decision, Config } from '@seanmozeik/tripwire';
209
+ import { allow, ask, deny, warn } from '@seanmozeik/tripwire';
210
+ import type { Config, Decision } from '@seanmozeik/tripwire';
207
211
  ```
208
212
 
209
213
  ## Development
210
214
 
211
215
  ```bash
212
- bun install
213
- bun run build # build dist/tripwire.js and dist/tripwire-cli.js
214
- bun run check # format, lint, typecheck
216
+ bun install --frozen-lockfile
217
+ bun run format:check
218
+ bun run lint
219
+ bun run typecheck
215
220
  bun test
221
+ bun run build
222
+ bun run verify
216
223
  ```
217
224
 
225
+ `bun run build` compiles `dist/tripwire` as one Bun bytecode executable. The entry module loads either the hook dispatcher or the interactive CLI. The build also emits `dist/tripwire-pi.js` for Pi and Oh My Pi.
226
+
227
+ `bun run verify` runs the format check, lint, type check, tests, and build. `prepublishOnly` calls the same local command.
228
+
218
229
  ## License
219
230
 
220
- MIT.
231
+ MIT. See [LICENSE](LICENSE).
package/dist/tripwire ADDED
Binary file
@@ -0,0 +1,4 @@
1
+ import{spawn as y}from"node:child_process";import{once as h}from"node:events";import{realpathSync as T}from"node:fs";import c from"node:path";import{fileURLToPath as P}from"node:url";var p=60000,k=(n=import.meta.url)=>{let t=P(n),e=t;try{e=T(t)}catch{}return c.join(c.dirname(e),"tripwire")},E=k(),a=(n,t,e)=>{for(let i of t){let r=n[i];if(typeof r==="string")return r}throw Error(`Pi ${e} is missing`)},R=(n,t)=>{let e=n[t];return Array.isArray(e)?e.filter((i)=>typeof i==="string"):[]},x=(n)=>{let{edits:t}=n;if(Array.isArray(t)){let e=[],i=[];for(let r of t){if(!u(r))continue;let o=s(r,"oldText")??s(r,"old_string"),l=s(r,"newText")??s(r,"new_string");if(o!==void 0)e.push(o);if(l!==void 0)i.push(l)}return{old_string:e.join(`
2
+ `),new_string:i.join(`
3
+ `)}}return{old_string:s(n,"oldText")??s(n,"old_string")??"",new_string:s(n,"newText")??s(n,"new_string")??s(n,"input")??s(n,"_input")??""}},_=(n)=>{let{input:t,toolName:e}=n;if(e==="bash"||e==="powershell")return[{command:a(t,["command"],`${e} command`)}];if(e==="read")return[{file_path:a(t,["path","file_path"],"read path")}];if(e==="write")return[{content:a(t,["content"],"write content"),file_path:a(t,["path","file_path"],"write path")}];if(e==="edit"){let i=s(t,"path")??s(t,"file_path"),o=R(t,"paths");if(o.length===0&&i!==void 0)o=[i];if(o.length===0)throw Error("Pi edit path is missing");let l=x(t),d=[];for(let m of o)d.push({file_path:m,...l});return d}return[t]},b=(n)=>{if(typeof n==="string")return n;if(!Array.isArray(n))return"";let t=[];for(let e of n)if(u(e)&&e.type==="text"&&typeof e.text==="string")t.push(e.text);return t.join(`
4
+ `)},S=(n)=>{let t=b(n.content);if(n.toolName==="bash"||n.toolName==="powershell")return{stdout:t,stderr:n.isError?t:"",interrupted:n.isError};if(n.toolName==="read")return{content:t};return{content:t,details:n.details,isError:n.isError}},f=(n,t,e)=>{let i={cwd:e,hook_event_name:t,tool_name:n.toolName,tool_use_id:n.toolCallId};if("content"in n)return[{...i,tool_input:n.input,tool_response:S(n)}];let r=[];for(let o of _(n))r.push({...i,tool_input:o});return r},g=async(n)=>{n.setEncoding("utf8");let t="";for await(let e of n)t+=String(e);return t},C=async(n,t)=>{let e=y(n,["--tripwire-hook"],{stdio:["pipe","pipe","pipe"]}),i={timedOut:!1},r=setTimeout(()=>{i.timedOut=!0,e.kill("SIGKILL")},p);e.stdin.end(JSON.stringify(t));try{let[o,l,d]=await Promise.all([h(e,"close"),g(e.stdout),g(e.stderr)]);if(i.timedOut)throw Error(`Tripwire exceeded its ${p}ms timeout`);return{exitCode:typeof o[0]==="number"?o[0]:1,stderr:d,stdout:l}}finally{clearTimeout(r)}},u=(n)=>typeof n==="object"&&n!==null&&!Array.isArray(n),s=(n,t)=>{let e=n[t];return typeof e==="string"?e:void 0},w=(n)=>{if(n.exitCode!==0)return n.stderr.trim()||`Tripwire exited ${n.exitCode}`;if(n.stdout.trim().length===0)return"Tripwire returned no decision";try{let t=JSON.parse(n.stdout);if(!u(t))return"Tripwire returned invalid JSON";let e=u(t.hookSpecificOutput)?t.hookSpecificOutput:void 0,i=(e===void 0?void 0:s(e,"permissionDecision"))??s(t,"permissionDecision")??s(t,"permission"),r=(e===void 0?void 0:s(e,"permissionDecisionReason"))??s(t,"permissionDecisionReason")??s(t,"reason")??"Blocked by Tripwire";if(i==="deny"||i==="ask")return r;if(s(t,"decision")==="block"||t.continue===!1)return r;return t.continue===!0||i==="allow"?void 0:"Tripwire returned an unrecognized response"}catch{return"Tripwire returned invalid JSON"}},v=(n,t=C)=>(e)=>{e.on("tool_call",async(i,r)=>{try{let o=f(i,"PreToolUse",r.cwd),l=o.length===1?o[0]:o;if(l===void 0)throw Error("Tripwire could not normalize the Pi tool call");let d=w(await t(n,l));if(d!==void 0)return{block:!0,reason:d};return{}}catch(o){return{block:!0,reason:`Tripwire failed closed: ${o instanceof Error?o.message:String(o)}`}}}),e.on("tool_result",async(i,r)=>{try{let[o]=f(i,"PostToolUse",r.cwd);if(o===void 0)throw Error("Tripwire could not normalize the Pi tool result");let l=w(await t(n,o));if(l!==void 0)r.ui.notify(`Tripwire stopped the session: ${l}`,"error"),r.abort()}catch(o){r.ui.notify(`Tripwire failed closed after tool use: ${o instanceof Error?o.message:String(o)}`,"error"),r.abort()}})},U=v(E);export{v as createTripwirePiExtension,U as default,f as hookInputs,k as resolveShippedHookPath,w as tripwirePiDenialReason};
package/package.json CHANGED
@@ -1,18 +1,44 @@
1
1
  {
2
2
  "name": "@seanmozeik/tripwire",
3
- "version": "0.6.7",
4
- "description": "Opinionated hooks dispatcher for AI coding agents with configurable safety rules",
3
+ "version": "0.7.0",
4
+ "description": "Configurable safety hooks for Claude Code, Codex, Cursor, Pi, and Oh My Pi",
5
+ "keywords": [
6
+ "ai-agents",
7
+ "bun",
8
+ "claude-code",
9
+ "codex",
10
+ "cursor",
11
+ "hooks",
12
+ "safety"
13
+ ],
14
+ "homepage": "https://github.com/seanmozeik/tripwire#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/seanmozeik/tripwire/issues"
17
+ },
5
18
  "license": "MIT",
19
+ "author": "Sean Mozeik",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/seanmozeik/tripwire.git"
23
+ },
6
24
  "bin": {
7
- "tripwire": "./dist/tripwire-cli.js",
8
- "tripwire-hook": "./dist/tripwire.js"
25
+ "tripwire": "./scripts/tripwire-cli",
26
+ "tripwire-hook": "./dist/tripwire"
9
27
  },
10
28
  "files": [
11
- "dist",
29
+ "dist/tripwire-pi.js",
30
+ "LICENSE",
12
31
  "package.json",
32
+ "scripts/tripwire-cli",
13
33
  "src",
14
34
  "README.md"
15
35
  ],
36
+ "os": [
37
+ "darwin"
38
+ ],
39
+ "cpu": [
40
+ "arm64"
41
+ ],
16
42
  "type": "module",
17
43
  "exports": {
18
44
  ".": "./src/index.ts"
@@ -22,34 +48,36 @@
22
48
  },
23
49
  "scripts": {
24
50
  "build": "bun scripts/build.ts",
25
- "prepublishOnly": "bun run build",
51
+ "prepublishOnly": "bun run verify",
26
52
  "check": "bun run format && bun run lint:fix && bun run typecheck",
27
53
  "format": "oxfmt --write .",
28
- "lint": "oxlint --tsconfig tsconfig.oxlint.json .",
29
- "lint:fix": "oxlint --format agent --tsconfig tsconfig.oxlint.json --fix .",
54
+ "format:check": "oxfmt --check .",
55
+ "lint": "oxlint .",
56
+ "lint:fix": "oxlint --format agent --fix .",
30
57
  "prepare": "bunx effect-tsgo patch",
31
58
  "test": "bun test",
32
- "typecheck": "tsc --noEmit"
59
+ "typecheck": "tsc --noEmit",
60
+ "verify": "bun run format:check && bun run lint && bun run typecheck && bun test && bun run build"
33
61
  },
34
62
  "dependencies": {
35
- "shell-quote": "^1.8.4"
63
+ "shell-quote": "1.10.0"
36
64
  },
37
65
  "devDependencies": {
38
- "@effect/platform-bun": "^4.0.0-beta.102",
39
- "@effect/tsgo": "^0.24.3",
40
- "@types/bun": "^1.3.14",
41
- "@types/shell-quote": "^1.7.5",
42
- "effect": "^4.0.0-beta.102",
43
- "oxfmt": "^0.53.0",
44
- "oxlint": "1.68.0",
45
- "oxlint-tsgolint": "^0.23.0",
46
- "typescript": "^7.0.2"
66
+ "@effect/platform-bun": "4.0.0-rc.112",
67
+ "@effect/tsgo": "0.38.0",
68
+ "@seanmozeik/de-clank": "0.1.7",
69
+ "bun-types": "1.4.0",
70
+ "effect": "4.0.0-rc.112",
71
+ "oxfmt": "0.65.0",
72
+ "oxlint": "1.80.0",
73
+ "oxlint-tsgolint": "7.0.2001",
74
+ "typescript": "7.0.2"
47
75
  },
48
76
  "peerDependencies": {
49
- "@effect/platform-bun": "^4.0.0-beta.102",
50
- "effect": "^4.0.0-beta.102"
77
+ "@effect/platform-bun": "4.0.0-rc.112",
78
+ "effect": "4.0.0-rc.112"
51
79
  },
52
80
  "engines": {
53
- "bun": ">=1.0"
81
+ "bun": ">=1.4"
54
82
  }
55
83
  }
@@ -0,0 +1,12 @@
1
+ #!/bin/sh
2
+
3
+ case $0 in
4
+ */*) launcher_directory=${0%/*} ;;
5
+ *)
6
+ launcher_path=$(command -v "$0") || exit 127
7
+ launcher_directory=${launcher_path%/*}
8
+ ;;
9
+ esac
10
+
11
+ [ -n "$launcher_directory" ] || launcher_directory=/
12
+ exec "$launcher_directory/tripwire-hook" --tripwire-force-cli "$@"