@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 +21 -0
- package/README.md +152 -141
- package/dist/tripwire +0 -0
- package/dist/tripwire-pi.js +4 -0
- package/package.json +50 -22
- package/scripts/tripwire-cli +12 -0
- package/src/cli.ts +64 -57
- package/src/dispatch.ts +322 -114
- package/src/index.ts +1 -1
- package/src/lib/bash.ts +106 -62
- package/src/lib/config.ts +93 -46
- package/src/lib/cursor.ts +336 -0
- package/src/lib/diff.ts +5 -2
- package/src/lib/event.ts +20 -21
- package/src/lib/install.ts +508 -136
- package/src/lib/log.ts +2 -3
- package/src/lib/secrets.ts +151 -88
- package/src/main.ts +31 -0
- package/src/pi-extension.ts +337 -0
- package/src/rules/bash-deny.ts +13 -3
- package/src/rules/bash-git.ts +32 -45
- package/src/rules/bash-network-install.ts +6 -3
- package/src/rules/bash-redirect.ts +5 -5
- package/src/rules/bash-scoped-rm.ts +1 -1
- package/src/rules/bash-tar-explosion.ts +16 -15
- package/src/rules/config-custom.ts +21 -15
- package/src/rules/lazy-code.ts +1 -1
- package/src/rules/path-protect.ts +73 -10
- package/src/rules/post-secret-scrub.ts +17 -6
- package/src/rules/read-protect.ts +5 -15
- package/src/rules/tool-policy.ts +54 -0
- package/dist/tripwire-cli.js +0 -11
- package/dist/tripwire-cli.js.jsc +0 -0
- package/dist/tripwire.js +0 -96
- package/dist/tripwire.js.jsc +0 -0
- package/src/rules/bash-tool-policy.ts +0 -146
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) [](LICENSE) [](https://bun.sh)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
```
|
|
8
|
-
$ tripwire test '
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
18
|
+
```bash
|
|
19
|
+
bun install --global @seanmozeik/tripwire
|
|
20
|
+
```
|
|
46
21
|
|
|
47
|
-
|
|
22
|
+
The package installs two commands:
|
|
48
23
|
|
|
49
|
-
|
|
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
|
-
|
|
27
|
+
Other platforms must build from source on the target host:
|
|
52
28
|
|
|
53
|
-
|
|
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
|
-
|
|
37
|
+
The source build uses the current host. A later release can add target-specific registry packages for other systems.
|
|
56
38
|
|
|
57
|
-
|
|
39
|
+
## Secret scanner requirement
|
|
58
40
|
|
|
59
|
-
|
|
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
|
-
|
|
43
|
+
Tripwire runs this command without a shell or temporary file:
|
|
62
44
|
|
|
63
|
-
```
|
|
64
|
-
|
|
45
|
+
```text
|
|
46
|
+
betterleaks stdin --report-format json --report-path -
|
|
65
47
|
```
|
|
66
48
|
|
|
67
|
-
|
|
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
|
-
##
|
|
51
|
+
## Install agent hooks
|
|
70
52
|
|
|
71
|
-
|
|
53
|
+
Run one installer after the package and Betterleaks are available:
|
|
72
54
|
|
|
73
55
|
```bash
|
|
74
|
-
tripwire install claude
|
|
75
|
-
tripwire install codex
|
|
76
|
-
tripwire install
|
|
77
|
-
tripwire install
|
|
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
|
-
|
|
64
|
+
The installers update these paths:
|
|
94
65
|
|
|
95
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
Cursor uses event-specific commands because some payloads do not include the configured event name:
|
|
110
83
|
|
|
111
84
|
```json
|
|
112
85
|
{
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
102
|
+
## Decisions and failure policy
|
|
103
|
+
|
|
104
|
+
Each applicable rule returns `allow`, `warn`, `ask`, or `deny`. The most restrictive result wins.
|
|
131
105
|
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
- `enforceConventionalCommits` (boolean, default `true`): require Conventional Commits format for commit messages.
|
|
108
|
+
Failure behavior depends on the stage and host:
|
|
136
109
|
|
|
137
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
118
|
+
Internal errors are written to `~/.claude/tripwire.log`. Logging failure does not change the hook response.
|
|
143
119
|
|
|
144
|
-
|
|
120
|
+
## Built-in checks
|
|
145
121
|
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
169
|
+
`toolPolicies` accepts these optional match fields:
|
|
190
170
|
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
git reset --hard HEAD~1
|
|
188
|
+
git reset --hard HEAD~1 # tripwire-allow
|
|
189
|
+
git reset --hard HEAD~1 # tripwire-allow:
|
|
196
190
|
```
|
|
197
191
|
|
|
198
|
-
The
|
|
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
|
-
##
|
|
194
|
+
## Test a rule
|
|
201
195
|
|
|
202
|
-
|
|
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,
|
|
206
|
-
import type {
|
|
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
|
|
214
|
-
bun run
|
|
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.
|
|
4
|
-
"description": "
|
|
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": "./
|
|
8
|
-
"tripwire-hook": "./dist/tripwire
|
|
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
|
|
51
|
+
"prepublishOnly": "bun run verify",
|
|
26
52
|
"check": "bun run format && bun run lint:fix && bun run typecheck",
|
|
27
53
|
"format": "oxfmt --write .",
|
|
28
|
-
"
|
|
29
|
-
"lint
|
|
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": "
|
|
63
|
+
"shell-quote": "1.10.0"
|
|
36
64
|
},
|
|
37
65
|
"devDependencies": {
|
|
38
|
-
"@effect/platform-bun": "
|
|
39
|
-
"@effect/tsgo": "
|
|
40
|
-
"@
|
|
41
|
-
"
|
|
42
|
-
"effect": "
|
|
43
|
-
"oxfmt": "
|
|
44
|
-
"oxlint": "1.
|
|
45
|
-
"oxlint-tsgolint": "
|
|
46
|
-
"typescript": "
|
|
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": "
|
|
50
|
-
"effect": "
|
|
77
|
+
"@effect/platform-bun": "4.0.0-rc.112",
|
|
78
|
+
"effect": "4.0.0-rc.112"
|
|
51
79
|
},
|
|
52
80
|
"engines": {
|
|
53
|
-
"bun": ">=1.
|
|
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 "$@"
|