@seanmozeik/tripwire 0.7.1 → 0.8.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 +20 -2
- package/dist/index.js +21 -27
- package/dist/tripwire-cli.js +1 -1
- package/dist/tripwire-hook.js +1 -1
- package/dist/tripwire-pi.js +30 -3
- package/dist/tripwire.js +107 -102
- package/dist/types/dispatch.d.ts +4 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/lib/bash/analyze.d.ts +3 -0
- package/dist/types/lib/bash/carriers.d.ts +11 -0
- package/dist/types/lib/bash/config.d.ts +17 -0
- package/dist/types/lib/bash/execution-types.d.ts +13 -0
- package/dist/types/lib/bash/execution.d.ts +13 -0
- package/dist/types/lib/bash/index.d.ts +8 -0
- package/dist/types/lib/bash/pipeline-inputs.d.ts +10 -0
- package/dist/types/lib/bash/static-command.d.ts +9 -0
- package/dist/types/lib/bash/types.d.ts +58 -0
- package/dist/types/lib/bash/values.d.ts +25 -0
- package/dist/types/lib/bash/wrappers.d.ts +21 -0
- package/dist/types/lib/config.d.ts +32 -0
- package/dist/types/lib/rule-actions.d.ts +27 -0
- package/dist/types/rules/bash-bypass.d.ts +5 -0
- package/dist/types/rules/bash-deny.d.ts +4 -4
- package/dist/types/rules/bash-git.d.ts +2 -2
- package/dist/types/rules/bash-network-install.d.ts +2 -2
- package/dist/types/rules/bash-redirect.d.ts +2 -2
- package/dist/types/rules/bash-scoped-rm.d.ts +2 -2
- package/dist/types/rules/bash-tar-explosion.d.ts +2 -2
- package/dist/types/rules/config-custom.d.ts +3 -3
- package/dist/types/rules/tool-policy.d.ts +2 -2
- package/package.json +4 -4
- package/dist/types/lib/bash.d.ts +0 -27
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ Tripwire includes checks for these operations:
|
|
|
126
126
|
|
|
127
127
|
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.
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
Tripwire parses Bash into one unbash AST. It follows executable commands through control flow, loops, groups, subshells, functions, aliases, substitutions, redirects, wrappers, and configured remote execution carriers. It denies a program when an executable branch or policy discriminator remains unknown.
|
|
130
130
|
|
|
131
131
|
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.
|
|
132
132
|
|
|
@@ -156,10 +156,20 @@ Personal workflow preferences belong in `~/.config/tripwire/config.json`. A miss
|
|
|
156
156
|
"allowedCommands": [
|
|
157
157
|
{ "pattern": "project-safe-tool", "message": "This command is allowed by personal config." }
|
|
158
158
|
],
|
|
159
|
-
"
|
|
159
|
+
"ruleActions": { "no-verify": "deny" },
|
|
160
|
+
"secretScanner": { "executable": "betterleaks", "timeoutMs": 5000 },
|
|
161
|
+
"shell": {
|
|
162
|
+
"executionCarrierAliases": [{ "command": ["just", "tailnet", "ssh"], "equivalentTo": "ssh" }]
|
|
163
|
+
}
|
|
160
164
|
}
|
|
161
165
|
```
|
|
162
166
|
|
|
167
|
+
An execution-carrier alias states that a fixed command path has the same nested-shell behavior as `ssh`. Tripwire inspects the remote command with the same parser and rules. A dynamic remote command remains denied.
|
|
168
|
+
|
|
169
|
+
`ruleActions` can set a listed workflow-policy rule to `allow`, `warn`, `ask`, or `deny`. Unknown rule names and safety-invariant rule names fail config validation instead of silently weakening policy.
|
|
170
|
+
|
|
171
|
+
Configurable workflow rules are `brew-mutation`, `chmod-777-recursive`, `defaults-write`, `dscl-mutate`, `launchctl-mutation`, `mas-mutation`, `no-gpg-sign`, `no-verify`, `osascript`, `pmset-write`, `rsync-delete`, `scutil-set`, `security-keychain-add-write`, `softwareupdate-install`, `sudo`, `systemsetup`, and `topgrade`.
|
|
172
|
+
|
|
163
173
|
`toolPolicies` accepts these optional match fields:
|
|
164
174
|
|
|
165
175
|
- `argumentsIncludeAll`
|
|
@@ -197,6 +207,14 @@ tripwire test --post --tool=Bash --stdout='example output'
|
|
|
197
207
|
|
|
198
208
|
The post-tool example requires Betterleaks.
|
|
199
209
|
|
|
210
|
+
For a longer workflow, inspect one file snapshot and execute the same bytes:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
tripwire run-script ./scripts/inspect-and-build.sh -- first-argument
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The runner reads the file once, checks the exact UTF-8 bytes, and sends those bytes to `/bin/bash -s`. Script arguments are included in value analysis. A denied or approval-required script does not start.
|
|
217
|
+
|
|
200
218
|
## Library API
|
|
201
219
|
|
|
202
220
|
```typescript
|
package/dist/index.js
CHANGED
|
@@ -1,35 +1,29 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var fe={allow:0,warn:1,ask:2,deny:3},a=(e)=>({kind:"allow",rule:e,message:""}),x=(e,t)=>({kind:"warn",rule:e,message:t}),y=(e,t)=>({kind:"ask",rule:e,message:t}),d=(e,t)=>({kind:"deny",rule:e,message:t}),D=(e)=>{let t=a("none");for(let n of e)if(fe[n.kind]>fe[t.kind])t=n;return t};import{BunRuntime as ps}from"@effect/platform-bun";import{Cause as L,Data as mt,Effect as S,Exit as J,Schema as q}from"effect";import{parse as xt,quote as H}from"shell-quote";var Et=["dist","build","_build","out","target",".next",".nuxt",".svelte-kit",".output",".astro",".angular",".vite",".parcel-cache",".turbo",".vercel",".netlify",".fly",".wrangler",".serverless","coverage",".nyc_output",".cache",".ruff_cache",".mypy_cache",".pytest_cache",".ty_cache",".tox","__pycache__",".venv","venv","node_modules",".gradle","DerivedData",".bundle",".cargo-target","tmp",".tmp",".state",".terraform",".yarn/cache",".yarn/install-state.gz",".pnpm-store",".bun"],ye=["/tmp","/var/tmp","/var/folders","/private/tmp","/private/var/tmp","/private/var/folders"],ke=new Set([">",">>","<","<<","<<<","<>",">&","<&","&>","&>>"]),be=new Set([";","&&","||","|","|&","&"]),Se=(e)=>typeof e==="string",E=(e)=>{if(typeof e==="object"&&"op"in e&&typeof e.op==="string")return e.op;return null},_t=(e)=>typeof e==="object"&&("comment"in e),Ct=(e)=>{try{let t=[...new Bun.Glob(e).scanSync({onlyFiles:!1,dot:!0})];if(t.length>0)return t}catch{}return[e]},Pt=(e)=>{if(Se(e))return[e];if(typeof e==="object"&&"op"in e&&e.op==="glob"&&"pattern"in e)return Ct(String(e.pattern));let t=E(e);if(t!==null){if(ke.has(t)||be.has(t))return[];return[`__op_${t}__`]}if(_t(e))return[];return["__tripwire_cmd_sub__"]},ge=(e,t)=>{let n=[],r=[],o=[],s=[],i=0;while(i<e.length){let g=e[i],p=E(g);if(p!==null&&ke.has(p)){let w=n.at(-1);if(w!==void 0&&/^[0-9]+$/.test(w)&&t.remaining>0)n.pop(),t.remaining-=1;let C=e[i+1];if(C!==void 0&&Se(C)){s.push({op:p,target:C}),i+=2;continue}i+=1;continue}for(let w of Pt(g))n.push(w);i+=1}if(n.length===0)return null;for(let g=1;g<n.length;g+=1){let p=n[g];if(p.startsWith("-")&&p!=="-")o.push(p);else r.push(p)}let l=n[0],u=l.lastIndexOf("/");return{head:u===-1?l:l.slice(u+1),tokens:n,args:r,flags:o,redirects:s,raw:n.join(" ")}},Ht=(e)=>`$${e}`,Tt=(e)=>{let t=[];for(let n=0;n<e.length;n+=1){let r=e[n],o=e[n+1];if(E(r)==="&"&&o!==void 0&&(E(o)===">"||E(o)===">>")){let s={op:E(o)===">"?"&>":"&>>"};t.push(s),n+=1;continue}if(E(r)===">"&&o!==void 0&&E(o)==="|"){t.push({op:">"}),n+=1;continue}t.push(r)}return t},Dt=(e)=>e.match(/(?<![\w$])\d+(?=[<>])/g)?.length??0,K=(e)=>{let t=/<<-?\s*(?:"(?<quoted>[^"]+)"|'(?<single>[^']+)'|(?<unquoted>[A-Za-z_][A-Za-z0-9_]*))/u.exec(e);return t?.groups?.quoted??t?.groups?.single??t?.groups?.unquoted??null},It=/(?:^|[|;&]\s*)(?:\/(?:usr\/bin|bin|usr\/local\/bin|opt\/homebrew\/bin)\/)?(?:sh|bash|zsh|dash|ksh|ash)(?:\s|$)/u,Z=(e)=>It.test(e),we=(e)=>{let t=e.split(`
|
|
3
|
-
|
|
4
|
-
`)},
|
|
5
|
-
`);
|
|
6
|
-
`))}return t},Ft=(e)=>{let t=e.split(`
|
|
7
|
-
`),n=[];for(let r=0;r<t.length;r+=1){let o=t[r],s=K(o);if(s===null||!Z(o))continue;let i=[];r+=1;while(r<t.length&&t[r].trim()!==s)i.push(t[r]),r+=1;if(i.length>0)n.push(i.join(`
|
|
8
|
-
`)
|
|
9
|
-
|
|
10
|
-
`)},k=(e)=>/(?<prefix>^|\s)#\s*tripwire-allow:[ \t]*\S[^\r\n]*/.test(we(e));import{readFileSync as fn}from"fs";import{homedir as gn}from"os";import{Cause as hn,Data as He,Effect as R,Schema as c}from"effect";var Pe=c.Struct({pattern:c.String,message:c.String,action:c.optional(c.Union([c.Literal("deny"),c.Literal("ask")])),requiresFlags:c.optional(c.Array(c.String)),forbidsFlagValues:c.optional(c.Array(c.Struct({flag:c.String,values:c.Array(c.String)})))}),mn=c.Struct({protectedBranches:c.optional(c.Array(c.String)),enforceConventionalCommits:c.optional(c.Boolean)}),pn=c.Struct({relative:c.optional(c.Array(c.String)),absolute:c.optional(c.Array(c.String))}),yn=c.Struct({argumentsIncludeAll:c.optional(c.Array(c.String)),argumentsStartWith:c.optional(c.Array(c.String)),shortFlagsIncludeAll:c.optional(c.Array(c.String))}),kn=c.Struct({rule:c.String,executables:c.Array(c.String),action:c.Union([c.Literal("deny"),c.Literal("warn")]),message:c.String,match:c.optional(yn)}),bn=c.Struct({executable:c.String,timeoutMs:c.Finite.check(c.isGreaterThan(0))}),Sn=c.Struct({git:c.optional(mn),safePaths:c.optional(pn),toolPolicies:c.optional(c.Array(kn)),blockedCommands:c.optional(c.Array(Pe)),allowedCommands:c.optional(c.Array(Pe)),secretScanner:c.optional(bn)}),U=`${gn()}/.config/tripwire/config.json`;class Te extends He.TaggedError("ConfigReadError"){}class De extends He.TaggedError("ConfigParseError"){}var wn=(e)=>e instanceof Error&&("code"in e)&&e.code==="ENOENT",Rn=(e)=>R.try({try:()=>fn(e,"utf8"),catch:(t)=>new Te({cause:t})}).pipe(R.catchTag("ConfigReadError",(t)=>wn(t.cause)?R.succeed(null):R.fail(t))),vn=(e)=>R.try({try:()=>JSON.parse(e),catch:(t)=>new De({cause:t})}),xn=(e)=>c.decodeUnknownEffect(Sn)(e,{onExcessProperty:"error"}),B=()=>({git:{protectedBranches:[],enforceConventionalCommits:!1},safePaths:{},toolPolicies:[],blockedCommands:[],allowedCommands:[],secretScanner:{executable:"betterleaks",timeoutMs:5000}}),z=(e)=>{let t=B();return{git:{protectedBranches:e.git?.protectedBranches??t.git.protectedBranches,enforceConventionalCommits:e.git?.enforceConventionalCommits??t.git.enforceConventionalCommits},safePaths:{...t.safePaths,...e.safePaths},toolPolicies:e.toolPolicies??t.toolPolicies,blockedCommands:e.blockedCommands??t.blockedCommands,allowedCommands:e.allowedCommands??t.allowedCommands,secretScanner:e.secretScanner??t.secretScanner}},G=(e=U)=>R.gen(function*(){let n=yield*Rn(e);if(n===null)return{ok:!0,config:B()};let r=yield*vn(n),o=yield*xn(r);return{ok:!0,config:z(o)}}).pipe(R.timeout(1000),R.catchCause((t)=>{let n={ok:!1,error:hn.pretty(t)};return R.succeed(n)})),En=(e=U)=>G(e).pipe(R.flatMap((t)=>t.ok?R.succeed(t.config):R.die(Error(`[tripwire] config load failed (${e}): ${t.error}`))));var Ae=new Set(["preToolUse","beforeShellExecution","beforeMCPExecution","beforeReadFile","beforeTabFileRead"]),ee=new Set(["postToolUse","postToolUseFailure","afterShellExecution","afterMCPExecution","afterFileEdit","afterTabFileEdit"]),te=(e)=>({kind:"cursor",eventName:e,post:ee.has(e)}),_n=(e)=>Ae.has(e)||ee.has(e),T=(e)=>typeof e==="object"&&e!==null&&!Array.isArray(e),m=(e,...t)=>{for(let n of t){let r=e[n];if(typeof r==="string")return r}return},ne=(e,...t)=>{for(let n of t)if(n in e)return e[n];return},Q=(e,t,...n)=>{let r=m(e,...n);if(r!==void 0)return r;if(typeof t==="string"&&n.includes("command"))return t;return T(t)?m(t,...n):void 0},Ie=(e,t)=>{if(e===void 0)throw Error(`Cursor hook payload missing ${t}`);return e},Cn=(e)=>{let t=e.toLowerCase();if(t==="bash"||t==="exec"||t==="shell"||t==="run_command")return"Bash";if(t==="read"||t==="read_file")return"Read";if(t==="write"||t==="write_file")return"Write";if(t==="edit"||t==="edit_file"||t==="multiedit"||t==="apply_patch")return"Edit";return e},Pn=(e)=>e==="Read"||e==="Write"||e==="Edit",Hn=(e,t)=>{let n;if(typeof e==="string")n=e;else if(T(e))n=m(e,"command","cmd");if(t&&n===void 0)throw Error("Cursor hook payload missing command");if(typeof e==="string")return{command:e};if(!T(e)||n===void 0)return e;return{...e,command:n}},Tn=(e,t,n)=>{let r=m(t,"file_path","filePath","path");if(n&&r===void 0)throw Error(`Cursor ${e} hook payload missing file path`);if(e==="Read")return{...t,file_path:r??""};if(e==="Write"){let o=m(t,"content","fileText","text");if(n&&o===void 0)throw Error("Cursor Write hook payload missing content");return{...t,file_path:r??"",content:o??""}}if(e==="Edit"){let o=m(t,"old_string","oldString"),s=m(t,"new_string","newString");if(n&&(o===void 0||s===void 0))throw Error("Cursor Edit hook payload missing oldString or newString");return{...t,file_path:r??"",old_string:o??"",new_string:s??""}}return t},Y=(e,t,n=!0)=>{if(e==="Bash")return Hn(t,n);if(!Pn(e))return t;if(!T(t)){if(n)throw Error(`Cursor ${e} hook payload is missing tool input`);return t}return Tn(e,t,n)},Fe=(e,t)=>{if(typeof t!=="string")return t;if(e==="Bash")return{stdout:t,stderr:""};if(e==="Read")return{content:t};return{content:t}},Dn=(e,t)=>{if(e==="beforeShellExecution"||e==="afterShellExecution")return"Bash";if(e==="beforeReadFile"||e==="beforeTabFileRead"||e==="afterFileEdit"||e==="afterTabFileEdit")return e.startsWith("before")?"Read":"Edit";let n=m(t,"tool_name","toolName");if(n!==void 0)return Cn(n);let r=ne(t,"tool_input","toolInput");if(m(t,"command","cmd")!==void 0||typeof r==="string"||T(r)&&m(r,"command","cmd")!==void 0)return"Bash";if(m(t,"file_path","filePath","path")!==void 0||T(r)&&m(r,"file_path","filePath","path")!==void 0)return"Read";return""},In=(e,t)=>{if(e==="Bash")return{command:Q(t,void 0,"command","cmd")};let n={file_path:m(t,"file_path","filePath","path")};if(e==="Write")n.content=m(t,"content","fileText","text");if(e==="Edit")n.old_string=m(t,"old_string","oldString"),n.new_string=m(t,"new_string","newString");return n},Fn=(e,t,n,r)=>{let o=ne(n,"tool_input","toolInput"),s=!r;if(e==="beforeShellExecution"||e==="afterShellExecution"){let i=Q(n,o,"command","cmd");return{command:s?Ie(i,"command"):i??""}}if(e==="beforeReadFile"||e==="beforeTabFileRead"||e==="afterFileEdit"||e==="afterTabFileEdit"){let i=Q(n,o,"file_path","filePath","path");return Y(t,{file_path:s?Ie(i,"file path"):i??"",...Array.isArray(n.edits)&&{edits:n.edits}},s)}if(o===void 0&&["Bash","Read","Write","Edit"].includes(t))return Y(t,In(t,n),s);return Y(t,o,s)},$n=(e,t,n)=>{if(e==="afterShellExecution")return Fe(t,m(n,"output")??"");let r=ne(n,"tool_response","toolResponse","tool_output","toolOutput","result","result_json");return Fe(t,r)},$e=(e,t,n)=>{let r=Dn(t,e),o=Fn(t,r,e,n),s=n?$n(t,r,e):void 0,i=m(e,"cwd"),l=m(e,"conversation_id","session_id"),u=m(e,"tool_use_id");return{event:{hook_event_name:n?"PostToolUse":"PreToolUse",...r.length>0&&{tool_name:r},...o!==void 0&&{tool_input:o},...s!==void 0&&{tool_response:s},...i!==void 0&&{cwd:i},...l!==void 0&&{session_id:l},...u!==void 0&&{tool_use_id:u}},host:te(t)}},We=(e,t)=>{if(t!==void 0&&!_n(t))throw Error(`Unknown Cursor hook event "${t}"`);if(!T(e)){if(t!==void 0)throw Error(`Cursor ${t} hook payload must be a JSON object`);return{event:e,host:{kind:"native"}}}let n=t??m(e,"hook_event_name");if(n!==void 0&&Ae.has(n))return $e(e,n,!1);if(n!==void 0&&ee.has(n))return $e(e,n,!0);return{event:e,host:{kind:"native"}}};import{Schema as b}from"effect";var re=b.Struct({hook_event_name:b.String,tool_name:b.optional(b.String),tool_input:b.optional(b.Unknown),tool_response:b.optional(b.Unknown),cwd:b.optional(b.String),session_id:b.optional(b.String),turn_id:b.optional(b.String),tool_use_id:b.optional(b.String)}),F=(e)=>typeof e==="object"&&e!==null&&!Array.isArray(e),Be=(e)=>F(e)&&typeof e.command==="string",Oe=(e)=>F(e)&&typeof e.file_path==="string"&&typeof e.old_string==="string"&&typeof e.new_string==="string",Le=(e)=>F(e)&&typeof e.file_path==="string"&&typeof e.content==="string",Me=(e)=>F(e)&&typeof e.file_path==="string",Ne=(e,t)=>{if(!F(t))return"";if(e==="Bash"){let n=typeof t.stdout==="string"?t.stdout:"",r=typeof t.stderr==="string"?t.stderr:"";return[n,r].filter((o)=>o.length>0).join(`
|
|
11
|
-
`)}if(e==="Read"){if(typeof t.content==="string")return t.content;let{file:n}=t;return F(n)&&typeof n.content==="string"?n.content:""}return typeof t.content==="string"?t.content:""};import{appendFileSync as An,mkdirSync as Wn}from"fs";import{homedir as Bn}from"os";import On from"path";var je=`${Bn()}/.claude/tripwire.log`;try{Wn(On.dirname(je),{recursive:!0})}catch{}var v=(e,t)=>{let n=new Date().toISOString(),r=t instanceof Error?t.stack??t.message:String(t);try{An(je,`[${n}] [${e}] ${r}
|
|
12
|
-
`)}catch{}};var Ln=(e)=>e.tokens.slice(1).join(" "),Ue=(e,...t)=>{let n=new Set(e.flags);return t.some((r)=>n.has(r))},Mn=[{rule:"unsupported-shell-structure",action:"deny",message:"Tripwire cannot inspect every executable branch in this shell structure. Rewrite it as simple commands joined with `;`, `&&`, or `||` so each command can be checked.",match:(e)=>e.head===N},{rule:"rm-rf-root",action:"deny",message:"rm -rf / is catastrophic. If the goal is cleaning a subdirectory, scope the path inside the project (e.g. ./dist).",match:(e)=>e.head==="rm"&&Ue(e,"-rf","-fr","-Rf","-fR")&&e.tokens.includes("/")},{rule:"rm-rf-home",action:"deny",message:"rm -rf on $HOME / ~ would erase the home directory. Refuse.",match:(e)=>e.head==="rm"&&Ue(e,"-rf","-fr","-Rf","-fR")&&e.tokens.some((t)=>/^(?<home>~|\$HOME|\$\{HOME\})$/.test(t))},{rule:"fork-bomb",action:"deny",message:"Fork bomb pattern detected. Refuse.",match:(e,t)=>/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/.test(t)},{rule:"source-script",action:"deny",message:"`source` / `.` executes a file in the current shell. Refuse arbitrary sourced scripts.",match:(e)=>(e.head==="source"||e.head===".")&&e.tokens.length>1},{rule:"dd-raw-device",action:"deny",message:"dd writing to a raw block device wipes the disk. Refuse.",match:(e)=>e.head==="dd"&&/\bof=\/dev\/(?<type>disk|sd|nvme|rdisk)/i.test(Ln(e))},{rule:"mkfs",action:"deny",message:"mkfs formats a filesystem. Refuse.",match:(e)=>/^mkfs(?<ext>\.[a-z0-9]+)?$/i.test(e.head)},{rule:"kill-all",action:"deny",message:"kill -9 -1 kills every process you own. Refuse.",match:(e)=>e.head==="kill"&&e.tokens.includes("-9")&&e.tokens.includes("-1")},{rule:"chmod-777-recursive",action:"deny",message:"Recursive chmod 777 makes everything world-writable. Use 755 for directories, 644 for files, scoped narrowly.",match:(e)=>e.head==="chmod"&&e.flags.some((t)=>t.includes("R"))&&e.tokens.includes("777")},{rule:"no-verify",action:"deny",message:"--no-verify skips git hooks. Per policy: never skip hooks. Fix the underlying issue.",match:(e)=>e.tokens.includes("--no-verify")},{rule:"no-gpg-sign",action:"deny",message:"Bypassing GPG signing is off-limits unless explicitly requested.",match:(e,t)=>/--no-gpg-sign\b|-c\s+commit\.gpgsign=false/.test(t)},{rule:"sudo",action:"ask",message:"sudo escalates privileges and is almost never needed in a coding session. If genuinely required, explain why; otherwise find a non-sudo path.",match:(e)=>e.head==="sudo"},{rule:"shutdown",action:"deny",message:"shutdown / reboot / halt control the machine. Refuse \u2014 system control should be done manually.",match:(e)=>["shutdown","reboot","halt","poweroff"].includes(e.head)},{rule:"launchctl-mutation",action:"deny",message:"launchctl load/unload/bootstrap/bootout/kickstart mutates system services. Refuse \u2014 surface the intent instead.",match:(e)=>e.head==="launchctl"&&typeof e.tokens[1]==="string"&&["load","unload","bootstrap","bootout","kickstart","enable","disable"].includes(e.tokens[1])},{rule:"defaults-write",action:"deny",message:"`defaults write` mutates macOS preferences. Refuse \u2014 surface the intent.",match:(e)=>e.head==="defaults"&&e.tokens[1]==="write"},{rule:"csrutil",action:"deny",message:"csrutil controls System Integrity Protection. Refuse.",match:(e)=>e.head==="csrutil"},{rule:"nvram",action:"deny",message:"nvram modifies firmware variables. Refuse.",match:(e)=>e.head==="nvram"},{rule:"diskutil-destructive",action:"deny",message:"diskutil eraseDisk / reformat / partitionDisk wipes disks. Refuse.",match:(e)=>e.head==="diskutil"&&typeof e.tokens[1]==="string"&&["eraseDisk","reformat","partitionDisk","eraseVolume","secureErase"].includes(e.tokens[1])},{rule:"tmutil-destructive",action:"deny",message:"tmutil delete / disablelocal touches Time Machine. Refuse.",match:(e)=>e.head==="tmutil"&&typeof e.tokens[1]==="string"&&["delete","disablelocal"].includes(e.tokens[1])},{rule:"osascript",action:"ask",message:"osascript runs arbitrary AppleScript and can do almost anything (move files, send emails, drive apps). Confirm with the user what you want done before running it.",match:(e)=>e.head==="osascript"},{rule:"topgrade",action:"deny",message:"topgrade upgrades everything (brew, mas, npm globals, rust, mise). System upgrades should be done manually.",match:(e)=>e.head==="topgrade"},{rule:"rsync-delete",action:"deny",message:"rsync --delete removes files at the destination. High blast radius \u2014 surface the intent instead.",match:(e)=>e.head==="rsync"&&e.flags.includes("--delete")},{rule:"softwareupdate-install",action:"deny",message:"`softwareupdate --install / -i / -d` triggers macOS system updates. Refuse \u2014 system updates should be done manually.",match:(e)=>e.head==="softwareupdate"&&e.flags.some((t)=>t==="--install"||t==="-i"||t==="-d"||t.startsWith("--download"))},{rule:"pmset-write",action:"deny",message:"`pmset` (with arguments) writes power-management settings. Read-only `pmset -g` is fine; mutations need the user.",match:(e)=>e.head==="pmset"&&e.tokens.length>1&&!e.tokens.includes("-g")&&!e.tokens.includes("-G")},{rule:"dscl-mutate",action:"deny",message:"`dscl . -create / -delete / -append / -change / -merge` modifies the local directory service (your user account, groups, etc.). Refuse.",match:(e)=>e.head==="dscl"&&e.tokens.some((t)=>["-create","-delete","-append","-change","-merge","-passwd"].includes(t))},{rule:"xattr-quarantine-bypass",action:"deny",message:"`xattr -d com.apple.quarantine` removes Gatekeeper's quarantine bit \u2014 the macOS protection against running untrusted binaries. Refuse.",match:(e)=>e.head==="xattr"&&e.tokens.includes("-d")&&e.tokens.some((t)=>t.includes("com.apple.quarantine"))},{rule:"spctl-disable",action:"deny",message:"`spctl --master-disable` / `--global-disable` disables Gatekeeper system-wide. Refuse.",match:(e)=>e.head==="spctl"&&e.flags.some((t)=>t==="--master-disable"||t==="--global-disable"||t==="--disable")},{rule:"kextload",action:"deny",message:"Loading a kernel extension (`kextload`, `kmutil load`) is a system-level mutation. Refuse \u2014 the user handles this manually.",match:(e)=>e.head==="kextload"||e.head==="kextunload"||e.head==="kmutil"&&(e.tokens[1]==="load"||e.tokens[1]==="unload")},{rule:"security-keychain-destructive",action:"deny",message:"`security delete-keychain / delete-generic-password / delete-internet-password / set-keychain-settings` mutates your Keychain (where CLIs store their secrets). Refuse \u2014 Keychain should be managed manually.",match:(e)=>e.head==="security"&&typeof e.tokens[1]==="string"&&["delete-keychain","delete-generic-password","delete-internet-password","delete-certificate","delete-identity","set-keychain-settings","unlock-keychain","lock-keychain","create-keychain"].includes(e.tokens[1])},{rule:"security-keychain-add-write",action:"ask",message:"`security add-generic-password / add-internet-password / add-certificate` writes to your Keychain. Other tools manage their own entries \u2014 confirm this is the right path before adding anything else manually.",match:(e)=>e.head==="security"&&typeof e.tokens[1]==="string"&&["add-generic-password","add-internet-password","add-certificate"].includes(e.tokens[1])},{rule:"systemsetup",action:"deny",message:"`systemsetup -set...` writes machine-wide settings (timezone, sleep, network time, restart-on-power-failure). Refuse.",match:(e)=>e.head==="systemsetup"&&e.tokens.some((t)=>t.startsWith("-set"))},{rule:"scutil-set",action:"deny",message:"`scutil --set` writes system configuration (computer name, hostname, LocalHostName). Refuse \u2014 read-only `scutil --get` is fine.",match:(e)=>e.head==="scutil"&&e.tokens.includes("--set")},{rule:"brew-mutation",action:"ask",message:"`brew install/uninstall/upgrade/untap` modifies the machine globally. Confirm before running.",match:(e)=>e.head==="brew"&&typeof e.tokens[1]==="string"&&["install","uninstall","upgrade","reinstall","untap","tap"].includes(e.tokens[1])},{rule:"mas-mutation",action:"ask",message:"`mas install/uninstall` changes installed Mac App Store apps. Confirm.",match:(e)=>e.head==="mas"&&typeof e.tokens[1]==="string"&&["install","uninstall","purchase"].includes(e.tokens[1])},{rule:"gh-destructive",action:"deny",message:"gh repo/release/issue delete is destructive on shared state. Refuse.",match:(e)=>e.head==="gh"&&typeof e.tokens[1]==="string"&&typeof e.tokens[2]==="string"&&["repo","release","issue","pr"].includes(e.tokens[1])&&["delete","destroy","remove"].includes(e.tokens[2])},{rule:"flyctl-destroy",action:"deny",message:"flyctl apps destroy / volumes destroy nukes deployed infra. Refuse.",match:(e)=>e.head==="flyctl"&&e.tokens.some((t)=>t==="destroy"||t==="delete")},{rule:"gcloud-delete",action:"deny",message:"`gcloud ... delete` affects cloud resources. Refuse.",match:(e)=>e.head==="gcloud"&&e.tokens.includes("delete")}],Nn=new Set(["unsupported-shell-structure","rm-rf-root","rm-rf-home","fork-bomb","dd-raw-device","mkfs","kill-all","diskutil-destructive","tmutil-destructive","shutdown","csrutil","nvram","kextload","spctl-disable","xattr-quarantine-bypass","topgrade","softwareupdate-install","systemsetup","scutil-set","security-keychain-destructive","no-verify","no-gpg-sign"]),ze=(e,t)=>{let n=k(t),r=[];for(let o of e)for(let s of Mn){if(!s.match(o,t))continue;if(n&&!Nn.has(s.rule))continue;r.push(s.action==="deny"?d(s.rule,s.message):y(s.rule,s.message));break}return r.length===0?a("bash-deny"):D(r)};var oe=(e)=>e.protectedBranches??[],jn=/^(?<type>feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?<scope>\([\w./\- ]+\))?!?:\s+\S/,Un=new Set(["-C","-c","--git-dir","--work-tree","--namespace","--super-prefix"]),zn=new Set(["--bare","--paginate","-p","--no-pager","--no-replace-objects","--literal-pathspecs","--glob-pathspecs","--noglob-pathspecs","--icase-pathspecs","--no-optional-locks","--exec-path","--html-path","--man-path","--info-path"]),Gn=(e)=>{if(e.head!=="git")return null;let t=e.tokens.slice(1),n=0;while(n<t.length){let r=t[n];if(r===void 0)break;if(Un.has(r)){n+=2;continue}if(zn.has(r)){n+=1;continue}if(r.startsWith("--git-dir=")||r.startsWith("--work-tree=")||r.startsWith("--namespace=")||r.startsWith("--super-prefix=")||r.startsWith("--exec-path=")){n+=1;continue}if(r.startsWith("-")){n+=1;continue}return{subcommand:r,subArgs:t.slice(n+1)}}return null},Jn=(e,t)=>{for(let n=0;n<e.length;n+=1){let r=e[n];if(r===void 0)break;if(r==="-m"||r==="--message"){let o=e[n+1];return o===void 0?null:j(o,t)}if(r.startsWith("--message="))return j(r.slice(10),t);if(/^-[a-zA-Z]*m[a-zA-Z]*$/.test(r)){let o=e[n+1];return o===void 0?null:j(o,t)}}return null},qn=(e,t)=>{let n=oe(t);for(let r of e)for(let o of n)if(r===o||r.endsWith(`:${o}`)||r.endsWith(`/${o}`))return o;return null},Vn=(e)=>e.filter((t)=>!t.startsWith("-")),Kn=(e)=>e.filter((t)=>t.startsWith("-")),h=(e,...t)=>{let n=new Set(e);return t.some((r)=>n.has(r))},Zn=({subArgs:e,positional:t})=>{if(h(e,"--global","--system"))return d("git-config-global","Modifying global / system git config is off-limits \u2014 that is your personal identity. Read-only `git config --get` is fine.");if(!h(e,"--get","-l","--list","--get-all","--get-regexp")&&t.length>=2)return d("git-config-write","Local git config writes should be done explicitly. To read a value, use `git config --get <key>`.");return a("bash-git")},Xn=({subArgs:e})=>{if(h(e,"--cached"))return a("bash-git");return y("git-rm","`git rm <path>` removes from the index AND the working tree. To untrack-only, use `git rm --cached <path>`. To delete the file separately, use `trash` / `rip`. Confirm intent.")},Yn=({subArgs:e,positional:t})=>{if(h(e,"--staged","-S")&&!h(e,"--worktree","-W"))return a("bash-git");if(t.length>0)return d("git-restore-discard","`git restore <path>` discards uncommitted changes in the working tree. Refuse \u2014 `git diff <path>` to inspect first, or `git stash push <path>` to preserve.");return a("bash-git")},Qn=({subArgs:e,positional:t})=>{if(h(e,"-b","-B"))return a("bash-git");if(h(e,"-f","--force"))return d("git-checkout-force","`git checkout -f` overwrites the working tree without preserving uncommitted changes. Refuse.");if(e.includes("--"))return d("git-checkout-discard","`git checkout -- <path>` discards uncommitted working-tree changes. Refuse \u2014 use `git stash push <path>` to preserve, or `git diff <path>` to inspect first.");let[n]=t;if(n!==void 0&&t.length===1&&(n==="."||n.startsWith("./")))return d("git-checkout-discard-all","`git checkout .` discards ALL uncommitted working-tree changes. Refuse \u2014 `git stash` to preserve, or `git diff` to inspect first.");return a("bash-git")},er=({subArgs:e})=>{if(h(e,"-f","--force","--discard-changes"))return d("git-switch-force","`git switch -f / --discard-changes` throws away uncommitted working-tree changes. Refuse.");return a("bash-git")},tr=({subArgs:e,flags:t,positional:n})=>{if(h(e,"--hard"))return d("git-reset-hard","`git reset --hard` discards all uncommitted changes AND moves HEAD. Refuse \u2014 describe the intent in chat. If undoing a published commit, `git revert <sha>` is safer.");if(h(e,"--keep"))return y("git-reset-keep","`git reset --keep` resets HEAD but preserves uncommitted local changes. Confirm intent.");if(n.length===0&&t.length===0)return a("bash-git");return y("git-reset-mixed","`git reset` moves HEAD. Confirm intent \u2014 if undoing a commit, `git revert` is usually safer.")},nr=({flags:e})=>{if(e.some((t)=>/^-[a-zA-Z]*[df]/.test(t)||t==="--force"))return d("git-clean-fd","`git clean -fd` deletes untracked files (often your in-progress work). Refuse \u2014 inspect with `git clean -dn` (dry run) first. If genuinely needed, append ` # tripwire-allow: <reason>`.");return a("bash-git")},rr=({subArgs:e,positional:t,config:n})=>{if(h(e,"--abort","--quit","--continue","--skip","--edit-todo"))return a("bash-git");if(h(e,"-i","--interactive"))return d("git-rebase-interactive","`git rebase -i` rewrites history interactively. Refuse \u2014 too easy to lose commits in the agent loop. If this is genuinely required, do it manually outside the agent.");let[r]=t,o=oe(n);if(r!==void 0&&o.includes(r))return y("git-rebase-onto-protected",`Rebasing onto \`${r}\` rewrites history of the current branch. \`git merge ${r}\` is usually safer. Confirm intent.`);return y("git-rebase","`git rebase` rewrites commit history. `git merge` is usually safer. Confirm intent.")},or=({subArgs:e})=>{if(h(e,"--abort","--quit","--continue","--skip"))return a("bash-git");return y("git-cherry-pick","`git cherry-pick` applies commits onto the current branch and can create conflicts. Confirm intent.")},sr=({subArgs:e})=>{if(h(e,"--abort","--continue","--quit"))return a("bash-git");return y("git-merge","`git merge <branch>` may create merge conflicts. Confirm intent.")},ir=({subArgs:e,config:t,heredocBodies:n})=>{if(h(e,"--amend"))return d("git-commit-amend","`git commit --amend` rewrites the last commit. If it has been pushed, this causes upstream divergence. Refuse \u2014 surface the intent.");let r=Jn(e,n),o=h(e,"-F","--file","-c","-C","--reuse-message","--reedit-message"),s=h(e,"--no-edit");if(r===null&&!o&&!s)return d("git-commit-no-message",'`git commit` without `-m "..."` opens an editor and hangs the agent. Use `git commit -m "<conventional message>"`.');if(r!==null&&t.enforceConventionalCommits===!0&&!jn.test(r))return d("git-commit-non-conventional",["Commit message must follow Conventional Commits format:"," `<type>(<scope>)?(!)?: <description>`","Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.","Examples:"," `fix(auth): handle expired token refresh`"," `feat: add bash-git rule`"," `chore: bump deps`",`Got: ${JSON.stringify(r)}`].join(`
|
|
13
|
-
`));if(h(e,"-a","--all")||e.some((i)=>/^-[a-zA-Z]*a[a-zA-Z]*$/.test(i)))return y("git-commit-auto-stage","`git commit -a / --all` auto-stages every tracked change. Explicit `git add <files>` first is usually clearer about what is being committed. Confirm.");return a("bash-git")},ar=({subArgs:e,flags:t,positional:n,config:r})=>{if(t.some((s)=>s==="--force"||s==="-f"||s.startsWith("--force-with-lease")))return d("git-force-push","Force push is forbidden. If a branch needs to be reset upstream, surface the intent \u2014 there is almost always a non-force path.");if(h(e,"--delete","--mirror")||e.some((s)=>s.startsWith(":")))return d("git-push-delete","Refusing to delete a remote branch via push. If genuinely needed, surface the intent.");let o=qn(n,r);if(o!==null)return d("git-push-protected",`Refusing to push directly to protected branch \`${o}\`. Open a PR instead: \`gh pr create\`.`);return a("bash-git")},cr=({subArgs:e,flags:t,positional:n,config:r})=>{let o=t.find((s)=>s==="-D"||s==="-d"||s==="--delete"||/^-[a-zA-Z]*D/.test(s)||/^-[a-zA-Z]*d/.test(s));if(o!==void 0){let s=n,i=new Set(oe(r)),l=s.find((u)=>i.has(u));if(l!==void 0)return d("git-branch-delete-protected",`Refusing to delete protected branch \`${l}\`.`);if(o==="-D"||o.includes("D"))return d("git-branch-force-delete",`\`git branch -D ${s.join(" ")}\` force-deletes branches even if unmerged (potential data loss). To delete a merged branch, use \`-d\`. To force, append \` # tripwire-allow: <reason>\`.`);return y("git-branch-delete",`\`git branch -d ${s.join(" ")}\` deletes a branch (merged-only check). Confirm intent.`)}if(h(e,"-m","-M","--move"))return y("git-branch-move","Renaming a branch can confuse pushed remotes. Confirm intent.");return a("bash-git")},lr=({subArgs:e})=>{if(h(e,"-d","--delete"))return d("git-tag-delete","`git tag -d` deletes a tag. Refuse \u2014 surface intent.");return a("bash-git")},dr=({subArgs:e})=>{let t=e[0]??"push";if(t==="drop"||t==="clear")return d("git-stash-drop",`\`git stash ${t}\` discards stashed work. Refuse \u2014 \`git stash list\` and \`git stash show\` to inspect first.`);return a("bash-git")},ur=({flags:e})=>{if(e.some((t)=>t.startsWith("--prune=")||t==="--aggressive"))return d("git-gc-prune","`git gc --prune=now` / `--aggressive` destroys reflog recovery options. Refuse.");return a("bash-git")},fr=({subArgs:e})=>{let[t]=e;if(t==="add"||t==="remove"||t==="rm"||t==="set-url"||t==="rename")return y("git-remote-mutate",`\`git remote ${t}\` changes which remote you're pushing to. Confirm \u2014 accidentally pointing at the wrong remote is high blast-radius.`);return a("bash-git")},Ge=({subcommand:e,subArgs:t})=>{let n=t[0]??"";if(["add","remove","rm","deinit","sync","set-url"].includes(n))return y("git-submodule-worktree-mutate",`\`git ${e} ${n}\` modifies repo structure. Confirm intent.`);return a("bash-git")},gr=new Map([["config",Zn],["add",()=>a("bash-git")],["mv",()=>a("bash-git")],["rm",Xn],["restore",Yn],["checkout",Qn],["switch",er],["reset",tr],["clean",nr],["rebase",rr],["cherry-pick",or],["merge",sr],["commit",ir],["push",ar],["branch",cr],["tag",lr],["stash",dr],["filter-branch",({subcommand:e})=>d("git-filter",`\`git ${e}\` rewrites entire repo history. Refuse.`)],["filter-repo",({subcommand:e})=>d("git-filter",`\`git ${e}\` rewrites entire repo history. Refuse.`)],["gc",ur],["update-ref",()=>d("git-update-ref","`git update-ref` directly mutates refs and bypasses normal git operations. Refuse.")],["remote",fr],["submodule",Ge],["worktree",Ge],["init",({subcommand:e})=>x(`git-${e}`,`\`git ${e}\` is allowed but unusual mid-session. Make sure this is what the user asked for.`)],["clone",({subcommand:e})=>x(`git-${e}`,`\`git ${e}\` is allowed but unusual mid-session. Make sure this is what the user asked for.`)]]),hr=(e,t,n)=>{let{subcommand:r,subArgs:o}=e,s=Kn(o),i=Vn(o);if(new Set(["status","diff","log","show","blame","rev-parse","rev-list","ls-files","ls-tree","cat-file","reflog","describe","shortlog","whatchanged","archive","bundle","fsck","fetch","ls-remote","help","version","grep","name-rev","merge-base","symbolic-ref","check-ignore","count-objects","verify-commit","verify-tag"]).has(r)){if(r==="reflog"&&(o[0]==="expire"||h(o,"--expire")))return d("git-reflog-expire","`git reflog expire` destroys git's recovery history. Refuse \u2014 surface the intent.");if(r==="symbolic-ref"&&i.length>=2)return d("git-symbolic-ref-write","`git symbolic-ref <name> <ref>` rewrites a symbolic ref. Refuse.");return a("bash-git")}let u=gr.get(r);if(u!==void 0)return u({subcommand:r,subArgs:o,flags:s,positional:i,config:t,heredocBodies:n});return x("git-unknown-subcommand",`\`git ${r}\` is not classified by tripwire. Allowing \u2014 flag if this looks like history-rewriting or data-loss territory.`)},Je=(e,t,n)=>{if(k(t))return a("bash-git");let r=Re(t);for(let o of e){let s=Gn(o);if(s===null)continue;let i=hr(s,n,r);if(i!==null&&i.kind!=="allow")return i}return a("bash-git")};var mr=new Set(["curl","wget","wget2","aria2c","xh"]),pr=new Set(["bash","sh","zsh","fish"]),yr=(e)=>{for(let t=0;t<e.length-1;t+=1){let n=e[t],r=e[t+1];if(n===void 0||r===void 0)continue;if(mr.has(n.head)&&pr.has(r.head))return!0}return!1},kr=[{head:"cargo",subcommand:"install",rule:"cargo-install",message:"Confirm before `cargo install <crate>`: this builds and installs arbitrary code from crates.io into ~/.cargo/bin globally."},{head:"go",subcommand:"install",rule:"go-install",message:"Confirm before `go install`: this fetches and installs arbitrary Go code globally."},{head:"gem",subcommand:"install",rule:"gem-install",message:"Confirm before `gem install`: pulls arbitrary code from rubygems.org."}],qe=(e,t)=>{if(k(t))return a("bash-network-install");if(yr(e))return d("curl-pipe-shell","Piping `curl` / `wget` directly into a shell runs whatever the remote URL serves. Refuse \u2014 download to a file, inspect, then run if appropriate. If you genuinely need this, append ` # tripwire-allow: <reason>` (and explain to the user what you're running).");for(let n of e)for(let r of kr)if(n.head===r.head&&n.tokens[1]===r.subcommand)return y(r.rule,r.message);return a("bash-network-install")};import{lstatSync as br,readlinkSync as Sr,realpathSync as wr}from"fs";import A from"path";var Rr=[{pattern:/(?<prefix>^|\/)\.env(?<ext>\.[^/]+)?$/,rule:"env-file",message:".env files hold secrets that should never be sent to the model. Refuse to write or edit. If an example is needed, create .env.example with redacted placeholders."},{pattern:/(?<prefix>^|\/)\.dev\.vars(?<ext>\.[^/]+)?$/,rule:"dev-vars",message:".dev.vars holds Cloudflare/Wrangler secrets. Do not modify."},{pattern:/(?<prefix>^|\/)\.ssh\//,rule:"ssh-dir",message:"Never write into ~/.ssh/. Refuse."},{pattern:/(?<prefix>^|\/)(?<key>id_rsa|id_ed25519|id_ecdsa|id_dsa)(?<pub>\.pub)?$/,rule:"ssh-key",message:"SSH key file. Refuse."},{pattern:/\.(?<ext>pem|key|p12|pfx)$/i,rule:"private-key",message:"Private key file. Refuse to overwrite. If generating a new key, use a different filename and let the user review."},{pattern:/(?<prefix>^|\/)secrets?\.(?<ext>json|ya?ml|toml|env)$/i,rule:"secrets-file",message:"Secrets file. Refuse."},{pattern:/(?<prefix>^|\/)\.aws\/credentials$/,rule:"aws-credentials",message:"AWS credentials file. Refuse."},{pattern:/(?<prefix>^|\/)\.netrc$/,rule:"netrc",message:".netrc holds host credentials. Refuse."}],Ve=(e)=>{try{return wr(e)}catch{return null}},se=(e,t=new Set)=>{if(t.has(e))return e;t.add(e);let n=Ve(e);if(n!==null)return n;try{if(br(e).isSymbolicLink()){let o=Sr(e);return se(A.resolve(A.dirname(e),o),t)}}catch{}let r=A.dirname(e);if(r===e)return e;return A.join(se(r,t),A.basename(e))},O=(e,t,n)=>{let r=A.resolve(e),o=t==="write"?se(r):Ve(r),s=[e,r];if(o!==null&&!s.includes(o))s.push(o);for(let i of s)for(let l of n)if(l.pattern.test(i))return l;return null},ie=(e)=>{let t=O(e.file_path,"write",Rr);if(t!==null)return d(t.rule,t.message);return a("path-protect")};var vr=[{rule:"redirect-env",pattern:/(?<prefix>^|\/)\.env(?<ext>\.[^/]+)?$/,message:"Refusing to write into a .env file via shell redirect / tee / cp / mv. .env files hold secrets \u2014 never overwrite from a tool call."},{rule:"redirect-dev-vars",pattern:/(?<prefix>^|\/)\.dev\.vars(?<ext>\.[^/]+)?$/,message:"Refusing to write into .dev.vars (Cloudflare/Wrangler secrets)."},{rule:"redirect-ssh",pattern:/(?<prefix>^|\/)\.ssh\//,message:"Refusing to write into ~/.ssh/ via shell."},{rule:"redirect-key",pattern:/\.(?<ext>pem|key|p12|pfx)$/i,message:"Refusing to overwrite a private-key-shaped file via shell."},{rule:"redirect-aws-credentials",pattern:/(?<prefix>^|\/)\.aws\/credentials$/,message:"Refusing to write into ~/.aws/credentials via shell."},{rule:"redirect-netrc",pattern:/(?<prefix>^|\/)\.netrc$/,message:"Refusing to write into ~/.netrc via shell."},{rule:"redirect-block-device",pattern:/^\/dev\/(?<type>sd|disk|nvme|rdisk)/i,message:"Redirecting into a raw block device wipes the disk. Refuse."}],ae=(e)=>{let t=O(e,"write",vr);if(t!==null)return d(t.rule,t.message);return null},Ke=(e,t)=>{if(k(t))return a("bash-redirect");for(let n of e){for(let r of n.redirects)if(r.op===">"||r.op===">>"){let o=ae(r.target);if(o!==null)return o}if(n.head==="tee")for(let r of n.args){let o=ae(r);if(o!==null)return o}if(n.head==="cp"||n.head==="mv"){let r=n.args.at(-1);if(r!==void 0){let o=ae(r);if(o!==null)return o}}}return a("bash-redirect")};var xr=(e,t)=>{let n=!1,r=[];for(let i of e.tokens.slice(1)){if(!n&&i==="--"){n=!0;continue}if(!n&&i.startsWith("-")&&i!=="-")continue;r.push(i)}let o=t.relative??[],s=t.absolute??[];return r.filter((i)=>!X(i,o,s))},Er=(e,t)=>{if(!e.tokens.includes("-delete"))return null;let n=[];for(let i of e.tokens.slice(1)){if(i.startsWith("-"))break;n.push(i)}let r=n.length===0?["."]:n,o=t.relative??[],s=t.absolute??[];return r.filter((i)=>!X(i,o,s))},Ze=(e,t,n)=>{if(k(t))return a("bash-scoped-rm");let r=[];for(let l of e){if(l.head==="rm"){let u=xr(l,n);if(u.length>0)r.push({kind:"rm",targets:u});continue}if(l.head==="find"){let u=Er(l,n);if(u!==null&&u.length>0)r.push({kind:"find -delete",targets:u})}}if(r.length===0)return a("bash-scoped-rm");let o=n.relative??[],s=n.absolute??[],i=r.map((l)=>` \u2022 ${l.kind} on: ${l.targets.map((u)=>JSON.stringify(u)).join(", ")}`).join(`
|
|
14
|
-
`);return d("destructive-outside-safe-paths",`Destructive deletion outside known-safe scopes is blocked. Use a recoverable deletion tool or limit the target to an ephemeral build, cache, state, or temporary directory:
|
|
15
|
-
${Ce(o,s)}
|
|
2
|
+
var Le={allow:0,warn:1,ask:2,deny:3},c=(e)=>({kind:"allow",rule:e,message:""}),E=(e,t)=>({kind:"warn",rule:e,message:t}),m=(e,t)=>({kind:"ask",rule:e,message:t}),u=(e,t)=>({kind:"deny",rule:e,message:t}),L=(e)=>{let t=c("none");for(let n of e)if(Le[n.kind]>Le[t.kind])t=n;return t};import{BunRuntime as mo}from"@effect/platform-bun";import{Cause as K,Data as Kt,Effect as w,Exit as se,Schema as ie}from"effect";var F="__tripwire_dynamic_shell_value__";var ae=(e)=>({start:e.pos,end:e.end}),an=(e)=>{let t=e.lastIndexOf("/");return t===-1?e:e.slice(t+1)},le=()=>({aliases:new Map,backgroundPidAvailable:!1,bindings:new Map,functions:new Map,temps:new Map}),y=(e)=>({aliases:new Map(e.aliases),backgroundPidAvailable:e.backgroundPidAvailable,bindings:new Map(e.bindings),functions:new Map(e.functions),temps:new Map(e.temps)}),ce=(e)=>e.type==="SingleQuoted"||e.type==="DoubleQuoted"||e.type==="AnsiCQuoted"||e.type==="LocaleString",Ne=(e)=>e.every((t)=>{switch(t.type){case"Literal":case"SingleQuoted":case"AnsiCQuoted":return!0;case"DoubleQuoted":case"LocaleString":return Ne(t.parts);case"SimpleExpansion":case"ParameterExpansion":case"CommandExpansion":case"ArithmeticExpansion":case"ProcessSubstitution":case"ExtendedGlob":case"BraceExpansion":return!1;default:return!1}}),_=(e)=>Ne(e.parts??[]),ze=(e)=>{if(e===void 0||(e.errors?.length??0)>0||e.commands.length!==1)return null;let[t]=e.commands;if(t===void 0||t.background===!0||t.redirects.length>0)return null;let{command:n}=t;if(n.type!=="Command"||n.name===void 0||!_(n.name))return null;if(n.prefix.length>0||n.suffix.some((r)=>!_(r)))return null;return{command:n,head:an(n.name.value),args:n.suffix.map((r)=>r.value)}},Ue=(e)=>{let t=e.parts??[];if(t.length!==1)return;let[n]=t;if(n?.type==="CommandExpansion")return n.script;if(n?.type!=="DoubleQuoted"||n.parts.length!==1)return;let[r]=n.parts;return r?.type==="CommandExpansion"?r.script:void 0},Ve=(e,t)=>e.command.name?.value.includes("/")===!1&&(t.aliases.has(e.head)||t.functions.has(e.head)),de=(e,t)=>{let n=ze(Ue(e));if(n===null||Ve(n,t))return null;if(n.head==="cat"&&n.args.length===0&&n.command.redirects.length===1){let[r]=n.command.redirects;if(r?.operator!=="<<"&&r?.operator!=="<<-")return null;if(r.heredocQuoted===!0||r.body===void 0||_(r.body))return(r.content??"").replace(/\n+$/u,"");return null}if(n.head==="printf"&&n.args.length===2){let[r,s]=n.args;if(r==="%s"||r===String.raw`%s\n`)return s??""}return null},Me=(e)=>["/tmp","/private/tmp","/var/tmp","/private/var/tmp","/var/folders","/private/var/folders"].some((t)=>e===t||e.startsWith(`${t}/`)),ln=(e)=>{let t=!1,n=null,r=[];for(let s=0;s<e.length;s+=1){let i=e[s];if(i==="-t")t=!0,s+=1;else if(i==="-p"||i==="--tmpdir")n=e[s+1]??"",s+=1;else if(i?.startsWith("--tmpdir=")===!0)n=i.slice(9);else if(i!==void 0&&!i.startsWith("-"))r.push(i)}return{explicitDirectory:n,positionals:r,systemTempTemplate:t}},Ge=(e,t)=>{let n=ze(Ue(e));if(n?.head!=="mktemp"||Ve(n,t)||["TMPDIR","TMP","TEMP"].some((a)=>t.bindings.has(a)||t.temps.has(a))||!n.args.some((a)=>a==="-d"||a==="--directory"||/^-[^-]*d/u.test(a)))return!1;let{explicitDirectory:r,positionals:s,systemTempTemplate:i}=ln(n.args);if(r!==null&&!Me(r))return!1;if(s.length===0||i||r!==null)return!0;let[o]=s;return s.length===1&&o!==void 0&&Me(o)},je=(e)=>{if(e.type==="SimpleExpansion")return/^\$(?<name>[A-Za-z_][A-Za-z0-9_]*|[0-9]+|!)$/u.exec(e.text)?.groups?.name??null;if(e.type==="ParameterExpansion"&&e.indirect!==!0&&e.length!==!0&&e.operator===void 0&&e.index===void 0&&e.slice===void 0&&e.replace===void 0&&/^(?:[A-Za-z_][A-Za-z0-9_]*|[0-9]+|!)$/u.test(e.parameter))return e.parameter;return null},qe=(e)=>{let t=[],n=(o,a)=>{for(let l of o)switch(l.type){case"Literal":case"SingleQuoted":case"AnsiCQuoted":{t.push({kind:"literal",value:l.value});break}case"DoubleQuoted":case"LocaleString":{if(!n(l.parts,!0))return!1;break}case"SimpleExpansion":case"ParameterExpansion":{let f=je(l);if(f===null)return!1;t.push({kind:"variable",quoted:a,variable:f});break}case"ArithmeticExpansion":case"BraceExpansion":case"CommandExpansion":case"ExtendedGlob":case"ProcessSubstitution":return!1;default:return!1}return!0};if(!n(e.parts??[],!1))return null;let r=t.filter((o)=>o.kind==="variable");if(r.length!==1)return null;let[s]=r;if(s===void 0)return null;let i=t.indexOf(s);return{prefix:t.slice(0,i).map((o)=>o.kind==="literal"?o.value:"").join(""),quoted:s.quoted,suffix:t.slice(i+1).map((o)=>o.kind==="literal"?o.value:"").join(""),variable:s.variable}},cn=(e)=>{let t=e.parts??[];if(t.length!==1||t[0]?.type!=="DoubleQuoted")return null;let n=t[0].parts;if(n.length<1||n.length>2)return null;let[r]=n,s=r===void 0?null:je(r);if(s===null)return null;let[,i]=n;if(i!==void 0&&i.type!=="Literal")return null;return{variable:s,suffix:i?.value??""}},Je=(e,t)=>{let n=cn(e);if(n===null||!t.temps.has(n.variable))return null;let{suffix:r,variable:s}=n;if(r!==""&&!r.startsWith("/")||r.split("/").includes("..")||r.includes("\x00"))return null;return{source:e.text,value:`/private/tmp/tripwire-provenance/${s}${r}`,kind:"trusted-temp-path",range:ae(e),quoted:!0,variable:s}},ue=(e,t)=>{let n=qe(e);if(n===null)return null;let{prefix:r,quoted:s,suffix:i,variable:o}=n,a=t.bindings.get(o);if(a===void 0||a.kind==="dynamic"||a.kind==="trusted-temp-path"||a.kind==="background-pid"&&(r!==""||i!=="")||i.split("/").includes("..")||r.split("/").includes(".."))return null;let l=`${r}${a.value}${i}`;if(!s&&(/\s/u.test(l)||l.includes("*")||l.includes("?")||l.includes("[")))return null;return{...a,source:e.text,value:l,range:ae(e),quoted:s,variable:o}},fe=(e,t)=>{let n=qe(e);if(!t.backgroundPidAvailable||n?.variable!=="!"||n.prefix!==""||n.suffix!=="")return null;return{source:e.text,value:"__tripwire_background_pid__",kind:"background-pid",range:ae(e),quoted:n.quoted,variable:"!"}};import{parse as we}from"unbash";var O=(e)=>({start:e.pos,end:e.end}),W=(e)=>{let t=e.lastIndexOf("/");return t===-1?e:e.slice(t+1)},M=(e)=>{if(e.name===void 0||e.prefix.length>0||e.redirects.length>0||!_(e.name)||e.suffix.some((t)=>!_(t)))return null;return[e.name,...e.suffix].map((t)=>({source:t.text,value:t.value,kind:"literal",range:O(t),quoted:(t.parts??[]).some((n)=>n.type==="SingleQuoted"||n.type==="DoubleQuoted"||n.type==="AnsiCQuoted"||n.type==="LocaleString")}))};var T=new Set(["sh","bash","zsh","dash","ksh","ash"]),Qe=new Set(["command","exec","env","time","nohup","setsid","nice","ionice","chronic","stdbuf","unbuffer","taskset","sudo","doas","xargs","watch"]),Ze={env:new Set(["-u","--unset","-C","--chdir","-S","--split-string","--block-signal"]),time:new Set(["-f","--format","-o","--output"]),nice:new Set(["-n","--adjustment"]),ionice:new Set(["-c","--class","-n","--classdata","-p","--pid"]),stdbuf:new Set(["-i","--input","-o","--output","-e","--error"]),sudo:new Set(["-u","--user","-g","--group","-C","--close-from","-D","--chdir","-h","--host","-p","--prompt","-r","--role","-t","--type","-U","--other-user","-R","--chroot","-T","--command-timeout"]),doas:new Set(["-a","-C","-u"]),xargs:new Set(["-I","-i","-J","-n","--max-args","-P","--max-procs","-s","--max-chars","-L","--max-lines","-E","--eof","-d","--delimiter","-a","--arg-file","--replace"]),watch:new Set(["-n","--interval"])},pe=(e,t,n)=>{let r=t;while(r<e.length){let s=e[r]?.value;if(s===void 0)break;if(n.has(s))r+=2;else if(s.includes("=")&&n.has(s.slice(0,s.indexOf("="))))r+=1;else if(s==="--")return r+1;else if(s.startsWith("-")&&s!=="-")r+=1;else break}return r},Q=(e)=>{let t=Object.hasOwn(Ze,e.head)?Ze[e.head]??new Set:new Set,n=pe(e.words,1,t);if(e.head==="env")while(/^[A-Za-z_][A-Za-z0-9_]*=/u.test(e.words[n]?.value??""))n+=1;return n},N=(e,t)=>{for(let n=1;n<e.length-1;n+=1){let r=e[n]?.value;if(r!==void 0&&(r===`-${t}`||r.startsWith("-")&&!r.startsWith("--")&&r.slice(1).includes(t)))return n+1}return null},X=(e,t,n=1)=>{for(let r=n;r<e.length;r+=1){let s=e[r]?.value;if(s!==void 0){if(t.has(s))return r+1<e.length?r+1:null;for(let i of t)if(s.startsWith(`${i}=`))return r}}return null},dn=new Set(["-e","--extension","-t","--type","-E","--exclude","-d","--max-depth","--min-depth","--exact-depth","-c","--color","--changed-within","--changed-before","-S","--size","-o","--owner","-j","--threads","-g","--glob","--format","--max-results","--ignore-file","--search-path","--base-directory","--path-separator","--and"]),un=(e)=>e==="/"||e==="~"||e==="."||e===".."||e.startsWith("/")||e.startsWith("~/")||e.startsWith("./")||e.startsWith("../")||/^\$\{?HOME\}?(?:\/|$)/u.test(e),Ke=(e)=>{if(e==="/")return 100;if(e==="~"||/^\$\{?HOME\}?(?:\/|$)/u.test(e))return 90;if(/^\/(?:etc|usr|bin|sbin|System|Library|var|boot|root|home)(?:\/|$)/u.test(e))return 80;if(e.startsWith("/Users/"))return 70;if(e.startsWith("/"))return 60;if(e.startsWith("../"))return 40;return 10},Xe=(e,t,n)=>{let r=[];for(let s=1;s<t;s+=1){let i=e[s];if(i!==void 0&&n==="fd"&&dn.has(i.value))s+=1;else if(i?.value.startsWith("-")===!0){if(n==="find")break}else if(i!==void 0&&un(i.value))r.push(i)}return r.sort((s,i)=>Ke(i.value)-Ke(s.value)),r[0]??{source:".",value:".",kind:"literal",range:e[0]?.range??{start:0,end:0},quoted:!1}},he=new Set(["-x","-X","--exec","--exec-batch"]),Ye=new Set(["{}","{/}","{//}","{.}","{/.}"]),et=new Set(["-exec","-execdir","-ok","-okdir"]),tt=new Set(["{}"]),nt=new Set(["run","proxy","err","test","summary"]),me={timeout:{valueFlags:new Set(["-s","--signal","-k","--kill-after"]),skipPositionals:1,shellFlag:!1},gtimeout:{valueFlags:new Set(["-s","--signal","-k","--kill-after"]),skipPositionals:1,shellFlag:!1},chroot:{valueFlags:new Set(["--userspec","--groups"]),skipPositionals:1,shellFlag:!1},flock:{valueFlags:new Set(["-w","--wait","--timeout","-E","--conflict-exit-code"]),skipPositionals:1,shellFlag:!0},su:{valueFlags:new Set,skipPositionals:0,shellFlag:!0}};var fn=new Set(["-B","-b","-c","-D","-E","-e","-F","-I","-i","-J","-L","-l","-m","-O","-o","-p","-Q","-R","-S","-W","-w"]),pn=(e,t)=>{let n=e[t];if(n===void 0)return{nextIndex:t,opaqueConfig:void 0,source:void 0};if(n.value==="-F"||n.value.startsWith("-F"))return{nextIndex:n.value==="-F"?t+1:t,opaqueConfig:n,source:void 0};let r=t,s;if(n.value==="-o")r+=1,s=e[r];else if(n.value.startsWith("-o"))s={...n,source:n.source.slice(2),value:n.value.slice(2)};let i=s===void 0?null:/^(?<key>[A-Za-z]+)(?:=|\s+)(?<source>[\s\S]+)$/u.exec(s.value),o=i?.groups?.key?.toLowerCase(),a=i?.groups?.source,l=s!==void 0&&a!==void 0&&a!=="none"&&o!==void 0&&["localcommand","proxycommand","remotecommand"].includes(o)?{...s,source:a,value:a}:void 0;return{nextIndex:r,opaqueConfig:void 0,source:l}},hn=(e)=>{let t=1;while(t<e.length){let r=e[t];if(r?.kind!=="literal")return null;if(r.value==="--"){t+=1;break}if(fn.has(r.value)){if(e[t+1]?.kind!=="literal")return null;t+=2}else if(r.value.startsWith("-")&&r.value!=="-")t+=1;else break}return e[t]?.kind==="literal"?t+1:null};class ge{#r;#e;constructor(e,t){this.#e=e,this.#r=[...t].toSorted((n,r)=>r.command.length-n.command.length)}isCarrierCommand(e){let t=M(e);return t!==null&&this.#o(t)!==null}inspect(e,t,n){let r=this.#o(e.words);if(r===null)return!1;this.#a(r.words,t,n);let s=hn(r.words);if(s===null){if(r.words.some((f)=>f.value==="-Q"||f.value==="-V"))return!0;return this.#e.addDiagnostic("dynamic-shell-source","Tripwire cannot identify the SSH destination and remote command safely.",e.range),!0}let i=r.words.slice(s);if(i.length===0){let f=r.words.some((p)=>p.value==="-N"||p.value==="-G"||p.value==="-Q"||p.value==="-V"||p.value==="-W"||p.value.startsWith("-W"));if(!n.inspectedPipelineInput&&!f)this.#e.addDiagnostic("dynamic-shell-source","An interactive remote shell has no command source that Tripwire can inspect.",e.range);return!0}let o=i.find((f)=>f.kind!=="literal");if(o!==void 0)return this.#e.addDiagnostic("dynamic-shell-source","The remote shell command is computed at runtime.",o.range),!0;if(n.inspectedPipelineInput&&T.has(W(i[0]?.value??""))&&N(i,"c")===null)return this.#e.emitSynthetic(i,e,t,{...n,inspectedPipelineInput:!0}),!0;let[a]=i,l=i.at(-1);if(a!==void 0&&l!==void 0){let f=i.map((p)=>p.value).join(" ");this.#e.inspectShellSource({source:f,value:f,kind:"literal",range:{start:a.range.start,end:l.range.end},quoted:!0},t,{...n,inspectedPipelineInput:!1})}return!0}#o(e){let[t]=e;if(t!==void 0&&W(t.value)==="ssh")return{kind:"ssh",words:e};for(let n of this.#r)if(n.command.every((r,s)=>e[s]?.value===r)&&n.command.length<=e.length){let[r]=e;if(r===void 0)return null;return{kind:n.equivalentTo,words:[{...r,source:n.equivalentTo,value:n.equivalentTo},...e.slice(n.command.length)]}}return null}#a(e,t,n){for(let r=1;r<e.length;r+=1){let s=pn(e,r);if(r=s.nextIndex,s.opaqueConfig!==void 0)this.#e.addDiagnostic("dynamic-shell-source","An explicit SSH configuration file may contain executable commands that Tripwire has not inspected.",s.opaqueConfig.range);else if(s.source!==void 0)this.#e.inspectShellSource(s.source,t,{...n,inspectedPipelineInput:!1})}}}var mn=(e,t)=>{let n=M(e),r=M(t);if(n===null||r===null||W(r[0]?.value??"")!=="base64"||r.length!==2||!["-d","-D","--decode"].includes(r[1]?.value??""))return null;let s=W(n[0]?.value??""),i=n.slice(1).map((o)=>o.value);if(s==="printf"&&i.length===2){let[o,a]=i;return o==="%s"||o===String.raw`%s\n`?a??null:null}if(s==="echo"){let o=i.filter((a)=>a!=="-n");return o.length===1?o[0]??null:null}return null},gn=(e)=>{if(e.length===0||e.length>1048576||!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(e))return null;let t=Buffer.from(e,"base64");if(t.toString("base64")!==e||t.includes(0))return null;try{return new TextDecoder("utf-8",{fatal:!0}).decode(t)}catch{return null}};class ye{#r;#e;constructor(e,t){this.#e=e,this.#r=t}inspect(e,t,n){let r=new Set;return this.#o(e,t,n,r),this.#a(e,t,n,r),r}#o(e,t,n,r){for(let s=0;s<e.length-1;s+=1){let i=e[s],o=e[s+1];if(i?.type==="Command"&&o?.type==="Command"&&o.name!==void 0&&T.has(W(o.name.value))){for(let a of i.redirects)if((a.operator==="<<"||a.operator==="<<-")&&a.content!==void 0)this.#e.inspectShellSource({source:a.content,value:a.content,kind:"literal",range:O(a),quoted:a.heredocQuoted===!0},t,n),r.add(s+1)}}}#a(e,t,n,r){for(let s=0;s<e.length-2;s+=1){let i=e[s],o=e[s+1],a=e[s+2];if(i?.type==="Command"&&o?.type==="Command"&&a?.type==="Command"&&this.#c(a)){let l=mn(i,o),f=l===null?null:gn(l);if(f!==null)this.#e.inspectShellSource({source:f,value:f,kind:"literal",range:O(i),quoted:!0},t,n),r.add(s+2)}}}#c(e){let t=M(e);if(t===null)return!1;if(T.has(W(t[0]?.value??"")))return N(t,"c")===null;return this.#r.isCarrierCommand(e)}}class Se{#r;#e;#o;constructor(e,t){this.#e=e,this.#r=new ge(e,t),this.#o=new ye(e,this.#r)}inspectShellInput(e,t,n,r){if(!T.has(e.head))return!1;let s=r.inspectedPipelineInput;for(let[i,o]of t.entries())if((o.operator==="<<"||o.operator==="<<-")&&o.content!==void 0){let a={source:o.content,value:o.content,kind:"literal",range:O(o),quoted:o.heredocQuoted===!0};this.#e.inspectShellSource(a,n,r),s=!0}else if(o.operator==="<<<")this.#e.inspectShellSource(e.redirects[i]?.target,n,r),s=!0;else if(o.operator==="<")this.#e.addDiagnostic("dynamic-shell-source","A shell wrapper reads program source from a file that Tripwire has not inspected.",O(o));return s}inspectWrapper(e,t,n,r=!1){if(e.head==="eval"||this.#r.inspect(e,t,n)||this.#a(e,t,n,r)||this.#c(e,t,n))return;this.#d(e,t,n)}inspectPipelineInputs(e,t,n){return this.#o.inspect(e,t,n)}#a(e,t,n,r){if(!T.has(e.head))return!1;let s=N(e.words,"c");if(s!==null)return this.#e.inspectShellSource(e.words[s],t,n),!0;if(!(r||e.tokens.includes("-n")||e.tokens.includes("--noexec")||e.tokens.includes("--help")||e.tokens.includes("--version")))this.#e.addDiagnostic("dynamic-shell-source","A shell wrapper has no inline program source that Tripwire can inspect.",e.range);return!0}#c(e,t,n){switch(e.head){case"trap":return this.#p(e,t,n),!0;case"script":{let r=X(e.words,new Set(["-c","--command"]));if(r!==null)this.#e.inspectShellSource(e.words[r],t,n);return!0}case"watch":return this.#y(e,t,n),!0;case"xargs":return this.#h(e,t,n),!0;default:return!1}}#y(e,t,n){let r=Q(e),s=e.words.slice(r);if(e.tokens.includes("-x")||e.tokens.includes("--exec")){this.#e.emitSynthetic(s,e,t,n);return}let[i]=s,o=s.at(-1),a=s.find((l)=>l.kind==="dynamic");if(a!==void 0){this.#e.addDiagnostic("dynamic-shell-source","watch receives command text that is computed at runtime.",a.range);return}if(i!==void 0&&o!==void 0){let l=s.map((f)=>f.value).join(" ");this.#e.inspectShellSource({source:l,value:l,kind:"literal",range:{start:i.range.start,end:o.range.end},quoted:!0},t,n)}}#d(e,t,n){if(Qe.has(e.head)){let r=Q(e);this.#e.emitSynthetic(e.words.slice(r),e,t,n)}else if(e.head==="fd"||e.head==="fdfind")this.#l(e,he,Ye,t,n);else if(e.head==="find"||e.head==="gfind")this.#l(e,et,tt,t,n);else if(e.head==="rtk")this.#u(e,t,n);else{let r=Object.hasOwn(me,e.head)?me[e.head]:void 0;if(r!==void 0)this.#S(e,r,t,n)}}#p(e,t,n){let[,r,s]=e.words,i=r?.value==="--"?s:r;if(i===void 0||i.value===""||i.value==="-"||i.value==="-p"||i.value==="-l")return;this.#e.inspectShellSource(i,t,n)}#h(e,t,n){let r=Q(e),s=e.words.slice(r),i=s[0]??{kind:"literal",quoted:!1,range:e.range,source:"echo",value:"echo"},o={kind:"dynamic",quoted:!1,range:e.range,source:"<xargs-input>",value:F};this.#e.emitSynthetic([i,...s.slice(1),o],e,t,n)}#l(e,t,n,r,s){for(let i=1;i<e.words.length;i+=1){let o=e.words[i];if(o!==void 0&&t.has(o.value)){let a=[],l=Xe(e.words,i,t===he?"fd":"find");for(let f=i+1;f<e.words.length;f+=1){let p=e.words[f];if(p===void 0||p.value===";"||p.value==="+")break;a.push(n.has(p.value)?l:p)}this.#e.emitSynthetic(a,e,r,s)}}}#u(e,t,n){let r=1;while(e.words[r]?.value.startsWith("-")===!0)r+=1;let s=e.words[r]?.value;if(s===void 0)return;if(nt.has(s)){let i=X(e.words,new Set(["-c","--command"]),r+1);if(i!==null){this.#e.inspectShellSource(e.words[i],t,n);return}this.#e.emitSynthetic(e.words.slice(r+1),e,t,n);return}this.#e.emitSynthetic(e.words.slice(r),e,t,n)}#S(e,t,n,r){if(t.shellFlag){let i=X(e.words,new Set(["-c","--command"]));if(i!==null){this.#e.inspectShellSource(e.words[i],n,r);return}}let s=pe(e.words,1,t.valueFlags);s+=t.skipPositionals,this.#e.emitSynthetic(e.words.slice(s),e,n,r)}}var R=(e)=>({start:e.pos,end:e.end}),yn=(e)=>{let t=e.lastIndexOf("/");return t===-1?e:e.slice(t+1)},Y=(e)=>{let[t,...n]=e,r=new Map;if(t===void 0)return r;for(let[s,i]of t)if(n.every((o)=>o.get(s)===i))r.set(s,i);return r},be=(e)=>{let[t,...n]=e;if(t===void 0)return le();return{aliases:Y([t.aliases,...n.map((r)=>r.aliases)]),backgroundPidAvailable:[t,...n].every((r)=>r.backgroundPidAvailable),bindings:Y([t.bindings,...n.map((r)=>r.bindings)]),functions:Y([t.functions,...n.map((r)=>r.functions)]),temps:Y([t.temps,...n.map((r)=>r.temps)])}},ke=(e,t)=>{e.backgroundPidAvailable=t.backgroundPidAvailable,e.aliases.clear();for(let[n,r]of t.aliases)e.aliases.set(n,r);e.bindings.clear();for(let[n,r]of t.bindings)e.bindings.set(n,r);e.functions.clear();for(let[n,r]of t.functions)e.functions.set(n,r);e.temps.clear();for(let[n,r]of t.temps)e.temps.set(n,r)},ee=(e,t,n)=>{for(let r of e.keys())if(!n.has(r)&&!t.has(r))e.delete(r);for(let[r,s]of t)if(!n.has(r))e.set(r,s)},Sn=(e,t,n)=>{ee(e.aliases,t.aliases,new Set),ee(e.functions,t.functions,new Set),ee(e.bindings,t.bindings,n),ee(e.temps,t.temps,n),e.backgroundPidAvailable=t.backgroundPidAvailable},rt=(e,t)=>{if(t.temps.delete(e.name),t.bindings.delete(e.name),e.trustedTemp)t.temps.set(e.name,{variable:e.name});else if(e.binding!==void 0)t.bindings.set(e.name,e.binding)},bn=(e,t,n)=>{if(e.append===!0||e.index!==void 0||e.array!==void 0||e.value===void 0)return!1;if(t?.kind==="literal")return _(e.value)||de(e.value,n)!==null||ue(e.value,n)!==null;return t?.kind==="background-pid"&&fe(e.value,n)!==null};class st{#r;#e=[];#o=[];#a=[];#c=[];#y=new WeakSet;#d=new Set;#p=new Set;#h=[];#l;#u;#S=1;#k=1;constructor(e,t){this.#r=e,this.#u=t,this.#l=new Se({addDiagnostic:(n,r,s)=>{this.#s(n,r,s)},emitSynthetic:(n,r,s,i)=>{this.#R(n,r,s,i)},inspectShellSource:(n,r,s)=>{this.#$(n,r,s)}},t.executionCarrierAliases??[])}analyze(e){let t=le(),n=(r,s)=>{t.bindings.set(r,{source:s,value:s,kind:"literal",range:{start:0,end:0},quoted:!0,variable:r})};if(this.#u.cwd!==void 0)n("PWD",this.#u.cwd);for(let[r,s]of(this.#u.positionalArguments??[]).entries())n((r+1).toString(),s);return this.#m(e,t,{inspectedPipelineInput:!1,source:this.#r,pipeline:void 0}),{source:this.#r,invocations:this.#e,redirects:this.#o,diagnostics:this.#a,hasBypass:this.#B()}}#s(e,t,n){this.#a.push({kind:e,message:t,range:n})}#m(e,t,n){if(this.#y.has(e))return;this.#y.add(e);let r=e.source??n.source;for(let s of e.errors??[])this.#s("parse-error",s.message,{start:s.pos,end:s.pos});this.#w(e.commands,t,{...n,source:r})}#w(e,t,n){for(let r of e)this.#v(r,t,n)}#v(e,t,n){if(this.#b(e.redirects,t,n),this.#n(e.command,t,n),e.background===!0)t.backgroundPidAvailable=!0}#n(e,t,n){if(this.#C(e,t,n)||this.#P(e,t,n)||this.#A(e,t,n))return;this.#s("parse-error",`Unknown Bash AST node: ${e.type}`,R(e))}#C(e,t,n){if(e.type==="Statement")return this.#v(e,t,n),!0;if(e.type==="Command")return this.#_(e,t,n),!0;if(e.type==="CompoundList")return this.#w(e.commands,t,n),!0;if(e.type==="Pipeline"){let r=this.#k;this.#k+=1;let s=this.#l.inspectPipelineInputs(e.commands,t,n);for(let[i,o]of e.commands.entries())this.#n(o,y(t),{...n,inspectedPipelineInput:s.has(i),pipeline:{id:r,index:i}});return!0}if(e.type==="AndOr"){let r=y(t);for(let s of e.commands)this.#n(s,r,n);return ke(t,be([t,r])),!0}return!1}#P(e,t,n){if(e.type==="If"){let r=y(t);this.#n(e.clause,r,n);let s=y(r);this.#n(e.then,s,n);let i=y(r);if(e.else!==void 0)this.#n(e.else,i,n);return ke(t,be([s,i])),!0}if(e.type==="For"||e.type==="Select"){let r=this.#t(e.name,t,n),s=e.wordlist.map((i)=>this.#t(i,t,n));if(r.kind==="literal"&&/^[A-Za-z_][A-Za-z0-9_]*$/u.test(r.value)&&s.length>0&&s.length<=32&&s.every((i)=>i.kind==="literal"))for(let i of s){let o=y(t);o.temps.delete(r.value),o.bindings.set(r.value,i),this.#n(e.body,o,n)}else{let i=y(t);i.temps.delete(r.value),i.bindings.delete(r.value),this.#n(e.body,i,n)}return!0}if(e.type==="ArithmeticFor")return this.#i(e.initialize,t,n),this.#i(e.test,t,n),this.#i(e.update,t,n),this.#n(e.body,y(t),n),!0;if(e.type==="While"){let r=y(t);return this.#n(e.clause,r,n),this.#n(e.body,r,n),!0}if(e.type==="Function"){let r=this.#t(e.name,t,n);if(r.kind!=="literal"||!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(r.value))this.#s("function","The function name is not static.",R(e));else t.functions.set(r.value,e);return!0}return!1}#A(e,t,n){if(e.type==="Subshell")return this.#n(e.body,y(t),n),!0;if(e.type==="BraceGroup")return this.#n(e.body,t,n),!0;if(e.type==="Case"){this.#t(e.word,t,n);let r=[y(t)];for(let s of e.items){for(let o of s.pattern)this.#t(o,t,n);let i=y(t);this.#n(s.body,i,n),r.push(i)}return ke(t,be(r)),!0}if(e.type==="Coproc"){if(e.name!==void 0)this.#t(e.name,t,n);return this.#b(e.redirects,t,n),this.#n(e.body,y(t),n),!0}if(e.type==="TestCommand")return this.#g(e.expression,t,n),!0;if(e.type==="ArithmeticCommand")return this.#i(e.expression,t,n),!0;return!1}#_(e,t,n){let r=e.prefix.flatMap((f)=>{let p=this.#I(f,t,n);return p===null?[]:[p]});if(e.name===void 0)for(let f of r)rt(f,t);let s=this.#b(e.redirects,t,n);if(e.name===void 0)return;let i=this.#t(e.name,t,n),o=e.suffix.flatMap((f)=>this.#x(f,t,n));if(i.kind==="literal"&&t.functions.has(i.value)){this.#T(i.value,o,r,t,n);return}if(i.kind==="literal"&&t.aliases.has(i.value)){this.#D(i.value,o,t,n,R(e));return}let a=this.#E([i,...o],s,R(e),n,!1);if(a===null)return;if(a.head==="alias"){this.#H(a,t);return}if(a.head==="unalias"){this.#F(a,t);return}let l=this.#l.inspectShellInput(a,e.redirects,t,n);this.#l.inspectWrapper(a,t,n,l),this.#O(a,t)}#I(e,t,n){for(let o of e.indexParts??[])this.#f(o,t,n);for(let o of e.array??[])this.#t(o,t,n);let r=e.value===void 0?void 0:this.#t(e.value,t,n);if(e.name===void 0)return null;let s=e.append!==!0&&e.index===void 0&&e.array===void 0&&e.value!==void 0&&Ge(e.value,t);return{binding:bn(e,r,t)?r:void 0,name:e.name,trustedTemp:s}}#x(e,t,n){let r=this.#t(e,t,n);if(r.kind!=="literal"||r.quoted||!/[*?[]/u.test(r.value))return[r];try{let s=[...new Bun.Glob(r.value).scanSync({onlyFiles:!1,dot:!0})];if(s.length===0)return[r];let i=[];for(let o of s)i.push({source:r.source,value:o,kind:r.kind,range:r.range,quoted:r.quoted});return i}catch{return[r]}}#t(e,t,n){for(let a of e.parts??[])this.#f(a,t,n);let r=Je(e,t);if(r!==null)return r;let s=fe(e,t);if(s!==null)return s;let i=ue(e,t);if(i!==null)return i;if(_(e))return{source:e.text,value:e.value,kind:"literal",range:R(e),quoted:(e.parts??[]).some((a)=>ce(a))};let o=de(e,t);if(o!==null)return{source:e.text,value:o,kind:"literal",range:R(e),quoted:!0};return{source:e.text,value:e.value,kind:"dynamic",range:R(e),quoted:(e.parts??[]).some((a)=>ce(a))}}#f(e,t,n){switch(e.type){case"Literal":case"SingleQuoted":case"AnsiCQuoted":case"SimpleExpansion":return;case"DoubleQuoted":case"LocaleString":{for(let r of e.parts)this.#f(r,t,n);return}case"ParameterExpansion":{this.#W(e,t,n);return}case"CommandExpansion":case"ProcessSubstitution":{if(e.script!==void 0)this.#m(e.script,y(t),n);return}case"ArithmeticExpansion":{this.#i(e.expression,t,n);return}case"ExtendedGlob":case"BraceExpansion":{for(let r of e.parts??[])this.#f(r,t,n);break}default:break}}#W(e,t,n){for(let r of e.indexParts??[])this.#f(r,t,n);if(e.operand!==void 0)this.#t(e.operand,t,n);if(e.slice!==void 0){if(this.#t(e.slice.offset,t,n),e.slice.length!==void 0)this.#t(e.slice.length,t,n)}if(e.replace!==void 0)this.#t(e.replace.pattern,t,n),this.#t(e.replace.replacement,t,n)}#i(e,t,n){if(e===void 0)return;switch(e.type){case"ArithmeticBinary":{this.#i(e.left,t,n),this.#i(e.right,t,n);return}case"ArithmeticUnary":{this.#i(e.operand,t,n);return}case"ArithmeticTernary":{this.#i(e.test,t,n),this.#i(e.consequent,t,n),this.#i(e.alternate,t,n);return}case"ArithmeticGroup":{this.#i(e.expression,t,n);return}case"ArithmeticWord":{for(let r of e.parts??[])this.#f(r,t,n);return}case"ArithmeticCommandExpansion":{if(e.script!==void 0)this.#m(e.script,y(t),n);break}default:break}}#g(e,t,n){switch(e.type){case"TestUnary":{this.#t(e.operand,t,n);return}case"TestBinary":{this.#t(e.left,t,n),this.#t(e.right,t,n);return}case"TestLogical":{this.#g(e.left,t,n),this.#g(e.right,t,n);return}case"TestNot":{this.#g(e.operand,t,n);return}case"TestGroup":{this.#g(e.expression,t,n);break}default:break}}#b(e,t,n){let r=[];for(let s of e){if(s.body!==void 0)this.#c.push(R(s.body)),this.#t(s.body,t,n);else if(s.content!==void 0){let l=n.source.indexOf(s.content,s.end);if(l!==-1)this.#c.push({start:l,end:l+s.content.length})}let i=s.target===void 0?{source:"",value:F,kind:"dynamic",range:R(s),quoted:!1}:this.#t(s.target,t,n),o;if(s.operator==="<<-")o="<<";else if(s.operator===">|")o=">";else o=s.operator;let a={op:o,target:i,range:R(s)};r.push(a),this.#o.push(a)}return r}#E(e,t,n,r,s){let[i]=e;if(i===void 0)return null;if(i.kind!=="literal")return this.#s("dynamic-executable","The executable name is computed at runtime.",i.range),null;let o=e.map((p)=>p.value),a=o.slice(1).filter((p)=>p.startsWith("-")&&p!=="-"),l=o.slice(1).filter((p)=>!p.startsWith("-")||p==="-"),f={id:this.#S,head:yn(i.value),words:e,tokens:o,args:l,flags:a,redirects:t,raw:s?e.map((p)=>p.source).join(" "):r.source.slice(n.start,n.end),range:n,pipeline:r.pipeline,synthetic:s};if(this.#S+=1,this.#e.push(f),f.head==="eval")this.#s("eval","`eval` can turn computed data into shell syntax.",n);return f}#T(e,t,n,r,s){let i=r.functions.get(e);if(i===void 0)return;if(this.#p.has(e)){this.#s("function",`Recursive function call: ${e}`,R(i));return}this.#p.add(e);let o=y(r),a=new Set(n.map((f)=>f.name));for(let f of n)rt(f,o);for(let f of o.bindings.keys())if(/^[0-9]+$/u.test(f))o.bindings.delete(f);for(let[f,p]of t.entries())o.bindings.set(String(f+1),p);let l={localNames:new Set};this.#h.push(l);try{this.#b(i.redirects,o,s),this.#n(i.body,o,s);let f=new Set([...a,...l.localNames]);for(let p of o.bindings.keys())if(/^[0-9]+$/u.test(p))f.add(p);Sn(r,o,f)}finally{this.#h.pop(),this.#p.delete(e)}}#D(e,t,n,r,s){let i=n.aliases.get(e);if(i===void 0)return;if(this.#d.has(e)){this.#s("alias",`Recursive alias expansion: ${e}`,s);return}this.#d.add(e);try{let o=we(i),[a]=o.commands,l=a?.command;if((o.errors?.length??0)>0||o.commands.length!==1||a?.background===!0||a?.redirects.length!==0||l?.type!=="Command"||l.name===void 0||l.prefix.length>0||l.redirects.length>0){this.#m(o,y(n),{...r,source:i,pipeline:void 0}),this.#s("alias",`Alias ${e} has compound expansion whose call arguments cannot be bound safely.`,s);return}let f={...r,source:i},p=[this.#t(l.name,n,f),...l.suffix.flatMap((v)=>this.#x(v,n,f)),...t],C={id:0,head:e,words:[],tokens:[],args:[],flags:[],redirects:[],raw:e,range:s,pipeline:r.pipeline,synthetic:!0};this.#R(p,C,n,r)}finally{this.#d.delete(e)}}#H(e,t){for(let n of e.words.slice(1))if(n.kind==="literal"){let r=n.value.indexOf("=");if(r!==-1){let s=n.value.slice(0,r);if(/^[A-Za-z_][A-Za-z0-9_]*$/u.test(s))t.aliases.set(s,n.value.slice(r+1));else this.#s("alias",`Invalid alias name: ${s}`,n.range)}}else this.#s("alias","An alias definition is computed at runtime.",n.range)}#F(e,t){if(e.tokens.includes("-a")){t.aliases.clear();return}for(let n of e.words.slice(1))if(n.kind==="literal")t.aliases.delete(n.value),this.#d.delete(n.value)}#R(e,t,n,r){let s=this.#E(e,[],t.range,{...r,pipeline:t.pipeline},!0);if(s!==null)this.#l.inspectWrapper(s,n,r,r.inspectedPipelineInput)}#$(e,t,n){if(e?.kind!=="literal"){this.#s("dynamic-shell-source","A shell wrapper receives source that is computed at runtime.",e?.range??{start:0,end:0});return}try{this.#m(we(e.value),y(t),{...n,inspectedPipelineInput:!1,source:e.value,pipeline:void 0})}catch(r){this.#s("parse-error",r instanceof Error?r.message:"The nested shell source could not be parsed.",e.range)}}#O(e,t){if(e.head==="export"&&e.flags.some((r)=>r==="-f"||/^-[^-]*f/u.test(r)))this.#s("function","Exported functions can execute in a nested shell outside the local call binding.",e.range);if(e.head==="unset"){for(let r of e.words.slice(1))if(/^[A-Za-z_][A-Za-z0-9_]*$/u.test(r.value))t.temps.delete(r.value),t.bindings.delete(r.value);return}if(!["export","readonly","declare","typeset","local"].includes(e.head))return;let n=this.#h.at(-1);for(let r of e.words.slice(1)){let s=/^(?<name>[A-Za-z_][A-Za-z0-9_]*)/u.exec(r.source)?.groups?.name;if(s!==void 0){if(e.head==="local")n?.localNames.add(s);t.temps.delete(s),t.bindings.delete(s)}}}#B(){let e=this.#r.split("");for(let t of this.#c)for(let n=t.start;n<t.end&&n<e.length;n+=1)if(e[n]!==`
|
|
3
|
+
`&&e[n]!=="\r")e[n]=" ";return/(?<prefix>^|\s)#\s*tripwire-allow:[ \t]*\S[^\r\n]*/u.test(e.join(""))}}var j=(e,t={})=>{try{return new st(e,t).analyze(we(e))}catch(n){return{source:e,invocations:[],redirects:[],diagnostics:[{kind:"parse-error",message:n instanceof Error?n.message:"The Bash parser failed.",range:{start:0,end:e.length}}],hasBypass:!1}}};var kn=["dist","build","_build","out","target",".next",".nuxt",".svelte-kit",".output",".astro",".angular",".vite",".parcel-cache",".turbo",".vercel",".netlify",".fly",".wrangler",".serverless","coverage",".nyc_output",".cache",".ruff_cache",".mypy_cache",".pytest_cache",".ty_cache",".tox","__pycache__",".venv","venv","node_modules",".gradle","DerivedData",".bundle",".cargo-target","tmp",".tmp",".state",".terraform",".yarn/cache",".yarn/install-state.gz",".pnpm-store",".bun"],it=["/tmp","/var/tmp","/var/folders","/private/tmp","/private/var/tmp","/private/var/folders"],wn=(e)=>e.startsWith("./")?e.slice(2):e,ve=(e,t=[],n=[])=>{if(e===""||e===F)return!1;let r=wn(e);if(r===".."||r.startsWith("../")||r.includes("/../"))return!1;for(let s of[...it,...n])if(r===s||r.startsWith(`${s}/`))return!0;for(let s of[...kn,...t])if(r===s||r.startsWith(`${s}/`))return!0;return!1},ot=(e=[],t=[])=>{let n={"build outputs":["dist","build","_build","out","target"],"js framework outputs":[".next",".nuxt",".svelte-kit",".output",".astro",".angular",".vite",".parcel-cache",".turbo",".vercel",".netlify",".fly",".wrangler",".serverless"],"tests / coverage":["coverage",".nyc_output"],caches:[".cache",".ruff_cache",".mypy_cache",".pytest_cache",".ty_cache",".tox"],"language / package":["__pycache__",".venv","venv","node_modules",".gradle","DerivedData",".bundle",".cargo-target"],"tmp / state":["tmp",".tmp",".state",...it],iac:[".terraform"],"bundler dev":[".yarn/cache",".yarn/install-state.gz",".pnpm-store",".bun"]};if(e.length>0)n["custom relative"]=e;if(t.length>0)n["custom absolute"]=t;return Object.entries(n).map(([r,s])=>` ${r}: ${s.join(", ")}`).join(`
|
|
4
|
+
`)};import{readFileSync as Rn}from"fs";import{homedir as Cn}from"os";import{Cause as Pn,Data as dt,Effect as x,Schema as d}from"effect";import{Schema as P}from"effect";var vn=P.Literal("ssh"),xn=P.Struct({command:P.Array(P.NonEmptyString).check(P.isMinLength(1),P.isMaxLength(16)),equivalentTo:vn}),at=P.Struct({executionCarrierAliases:P.optional(P.Array(xn).check(P.isMaxLength(64)))});import{Schema as S}from"effect";var b=S.Literals(["allow","warn","ask","deny"]),xe=S.Struct({"brew-mutation":S.optional(b),"chmod-777-recursive":S.optional(b),"defaults-write":S.optional(b),"dscl-mutate":S.optional(b),"launchctl-mutation":S.optional(b),"mas-mutation":S.optional(b),"no-gpg-sign":S.optional(b),"no-verify":S.optional(b),osascript:S.optional(b),"pmset-write":S.optional(b),"rsync-delete":S.optional(b),"scutil-set":S.optional(b),"security-keychain-add-write":S.optional(b),"softwareupdate-install":S.optional(b),sudo:S.optional(b),systemsetup:S.optional(b),topgrade:S.optional(b)}),En=(e)=>Object.hasOwn(xe.fields,e),lt=(e,t)=>{let n=En(e.rule)?t[e.rule]:void 0;if(n===void 0||n===e.kind)return e;if(n==="allow")return c(e.rule);if(n==="warn")return E(e.rule,e.message);if(n==="ask")return m(e.rule,e.message);return u(e.rule,e.message)};var ct=d.Struct({pattern:d.String,message:d.String,action:d.optional(d.Union([d.Literal("deny"),d.Literal("ask")])),requiresFlags:d.optional(d.Array(d.String)),forbidsFlagValues:d.optional(d.Array(d.Struct({flag:d.String,values:d.Array(d.String)})))}),An=d.Struct({protectedBranches:d.optional(d.Array(d.String)),enforceConventionalCommits:d.optional(d.Boolean)}),_n=d.Struct({relative:d.optional(d.Array(d.String)),absolute:d.optional(d.Array(d.String))}),In=d.Struct({argumentsIncludeAll:d.optional(d.Array(d.String)),argumentsStartWith:d.optional(d.Array(d.String)),shortFlagsIncludeAll:d.optional(d.Array(d.String))}),Wn=d.Struct({rule:d.String,executables:d.Array(d.String),action:d.Union([d.Literal("deny"),d.Literal("warn")]),message:d.String,match:d.optional(In)}),Tn=d.Struct({executable:d.String,timeoutMs:d.Finite.check(d.isGreaterThan(0))}),Dn=d.Struct({git:d.optional(An),safePaths:d.optional(_n),toolPolicies:d.optional(d.Array(Wn)),blockedCommands:d.optional(d.Array(ct)),allowedCommands:d.optional(d.Array(ct)),ruleActions:d.optional(xe),secretScanner:d.optional(Tn),shell:d.optional(at)}),te=`${Cn()}/.config/tripwire/config.json`;class ut extends dt.TaggedError("ConfigReadError"){}class ft extends dt.TaggedError("ConfigParseError"){}var Hn=(e)=>e instanceof Error&&("code"in e)&&e.code==="ENOENT",Fn=(e)=>x.try({try:()=>Rn(e,"utf8"),catch:(t)=>new ut({cause:t})}).pipe(x.catchTag("ConfigReadError",(t)=>Hn(t.cause)?x.succeed(null):x.fail(t))),$n=(e)=>x.try({try:()=>JSON.parse(e),catch:(t)=>new ft({cause:t})}),On=(e)=>d.decodeUnknownEffect(Dn)(e,{onExcessProperty:"error"}),q=()=>({git:{protectedBranches:[],enforceConventionalCommits:!1},safePaths:{},toolPolicies:[],blockedCommands:[],allowedCommands:[],ruleActions:{},secretScanner:{executable:"betterleaks",timeoutMs:5000},shell:{executionCarrierAliases:[]}}),J=(e)=>{let t=q();return{git:{protectedBranches:e.git?.protectedBranches??t.git.protectedBranches,enforceConventionalCommits:e.git?.enforceConventionalCommits??t.git.enforceConventionalCommits},safePaths:{...t.safePaths,...e.safePaths},toolPolicies:e.toolPolicies??t.toolPolicies,blockedCommands:e.blockedCommands??t.blockedCommands,allowedCommands:e.allowedCommands??t.allowedCommands,ruleActions:{...t.ruleActions,...e.ruleActions},secretScanner:e.secretScanner??t.secretScanner,shell:{executionCarrierAliases:e.shell?.executionCarrierAliases??t.shell.executionCarrierAliases}}},ne=(e=te)=>x.gen(function*(){let n=yield*Fn(e);if(n===null)return{ok:!0,config:q()};let r=yield*$n(n),s=yield*On(r);return{ok:!0,config:J(s)}}).pipe(x.timeout(1000),x.catchCause((t)=>{let n={ok:!1,error:Pn.pretty(t)};return x.succeed(n)})),Bn=(e=te)=>ne(e).pipe(x.flatMap((t)=>t.ok?x.succeed(t.config):x.die(Error(`[tripwire] config load failed (${e}): ${t.error}`))));import{Result as Ln,Schema as re}from"effect";var Mn=re.Record(re.String,re.Unknown),gt=new Set(["preToolUse","beforeShellExecution","beforeMCPExecution","beforeReadFile","beforeTabFileRead"]),Ce=new Set(["postToolUse","postToolUseFailure","afterShellExecution","afterMCPExecution","afterFileEdit","afterTabFileEdit"]),Pe=(e)=>({kind:"cursor",eventName:e,post:Ce.has(e)}),Nn=(e)=>gt.has(e)||Ce.has(e),B=(e)=>typeof e==="object"&&e!==null&&!Array.isArray(e),g=(e,...t)=>{for(let n of t){let r=e[n];if(typeof r==="string")return r}return},Ae=(e,...t)=>{for(let n of t)if(n in e)return e[n];return},Re=(e,t,...n)=>{let r=g(e,...n);if(r!==void 0)return r;if(typeof t==="string"&&n.includes("command"))return t;return B(t)?g(t,...n):void 0},pt=(e,t)=>{if(e===void 0)throw Error(`Cursor hook payload missing ${t}`);return e},zn=(e)=>{let t=e.toLowerCase();if(t==="bash"||t==="exec"||t==="shell"||t==="run_command")return"Bash";if(t==="read"||t==="read_file")return"Read";if(t==="write"||t==="write_file")return"Write";if(t==="edit"||t==="edit_file"||t==="multiedit"||t==="apply_patch")return"Edit";return e},Un=(e)=>e==="Read"||e==="Write"||e==="Edit",Vn=(e,t)=>{let n;if(typeof e==="string")n=e;else if(B(e))n=g(e,"command","cmd");if(t&&n===void 0)throw Error("Cursor hook payload missing command");if(typeof e==="string")return{command:e};if(!B(e)||n===void 0)return e;return{...e,command:n}},Gn=(e,t,n)=>{let r=g(t,"file_path","filePath","path");if(n&&r===void 0)throw Error(`Cursor ${e} hook payload missing file path`);if(e==="Read")return{...t,file_path:r??""};if(e==="Write"){let s=g(t,"content","fileText","text");if(n&&s===void 0)throw Error("Cursor Write hook payload missing content");return{...t,file_path:r??"",content:s??""}}if(e==="Edit"){let s=g(t,"old_string","oldString"),i=g(t,"new_string","newString");if(n&&(s===void 0||i===void 0))throw Error("Cursor Edit hook payload missing oldString or newString");return{...t,file_path:r??"",old_string:s??"",new_string:i??""}}return t},Ee=(e,t,n=!0)=>{if(e==="Bash")return Vn(t,n);if(!Un(e))return t;if(!B(t)){if(n)throw Error(`Cursor ${e} hook payload is missing tool input`);return t}return Gn(e,t,n)},ht=(e,t)=>{if(typeof t!=="string")return t;if(e==="Bash")return{stdout:t,stderr:""};if(e==="Read")return{content:t};return{content:t}},jn=(e,t)=>{if(e==="beforeShellExecution"||e==="afterShellExecution")return"Bash";if(e==="beforeReadFile"||e==="beforeTabFileRead"||e==="afterFileEdit"||e==="afterTabFileEdit")return e.startsWith("before")?"Read":"Edit";let n=g(t,"tool_name","toolName");if(n!==void 0)return zn(n);let r=Ae(t,"tool_input","toolInput");if(g(t,"command","cmd")!==void 0||typeof r==="string"||B(r)&&g(r,"command","cmd")!==void 0)return"Bash";if(g(t,"file_path","filePath","path")!==void 0||B(r)&&g(r,"file_path","filePath","path")!==void 0)return"Read";return""},qn=(e,t)=>{if(e==="Bash")return{command:Re(t,void 0,"command","cmd")};let n={file_path:g(t,"file_path","filePath","path")};if(e==="Write")return{...n,content:g(t,"content","fileText","text")};if(e==="Edit")return{...n,old_string:g(t,"old_string","oldString"),new_string:g(t,"new_string","newString")};return n},Jn=(e,t,n,r)=>{let s=re.decodeUnknownResult(Mn)(n);if(Ln.isFailure(s))throw Error("Cursor sent an invalid tool event");let i=s.success,o=Ae(i,"tool_input","toolInput"),a=!r;if(e==="beforeShellExecution"||e==="afterShellExecution"){let l=Re(i,o,"command","cmd");return{command:a?pt(l,"command"):l??""}}if(e==="beforeReadFile"||e==="beforeTabFileRead"||e==="afterFileEdit"||e==="afterTabFileEdit"){let l=Re(i,o,"file_path","filePath","path");return Ee(t,{file_path:a?pt(l,"file path"):l??"",...Array.isArray(i.edits)&&{edits:i.edits}},a)}if(o===void 0&&["Bash","Read","Write","Edit"].includes(t))return Ee(t,qn(t,i),a);return Ee(t,o,a)},Zn=(e,t,n)=>{if(e==="afterShellExecution")return ht(t,g(n,"output")??"");let r=Ae(n,"tool_response","toolResponse","tool_output","toolOutput","result","result_json");return ht(t,r)},mt=(e,t,n)=>{let r=jn(t,e),s=Jn(t,r,e,n),i=n?Zn(t,r,e):void 0,o=g(e,"cwd"),a=g(e,"conversation_id","session_id"),l=g(e,"tool_use_id");return{event:{hook_event_name:n?"PostToolUse":"PreToolUse",...r.length>0&&{tool_name:r},...s!==void 0&&{tool_input:s},...i!==void 0&&{tool_response:i},...o!==void 0&&{cwd:o},...a!==void 0&&{session_id:a},...l!==void 0&&{tool_use_id:l}},host:Pe(t)}},yt=(e,t)=>{if(t!==void 0&&!Nn(t))throw Error(`Unknown Cursor hook event "${t}"`);if(!B(e)){if(t!==void 0)throw Error(`Cursor ${t} hook payload must be a JSON object`);return{event:e,host:{kind:"native"}}}let n=t??g(e,"hook_event_name");if(n!==void 0&>.has(n))return mt(e,n,!1);if(n!==void 0&&Ce.has(n))return mt(e,n,!0);return{event:e,host:{kind:"native"}}};import{Schema as k}from"effect";var _e=k.Struct({hook_event_name:k.String,tool_name:k.optional(k.String),tool_input:k.optional(k.Unknown),tool_response:k.optional(k.Unknown),cwd:k.optional(k.String),session_id:k.optional(k.String),turn_id:k.optional(k.String),tool_use_id:k.optional(k.String)}),z=(e)=>typeof e==="object"&&e!==null&&!Array.isArray(e),St=(e)=>z(e)&&typeof e.command==="string",bt=(e)=>z(e)&&typeof e.file_path==="string"&&typeof e.old_string==="string"&&typeof e.new_string==="string",kt=(e)=>z(e)&&typeof e.file_path==="string"&&typeof e.content==="string",wt=(e)=>z(e)&&typeof e.file_path==="string",vt=(e,t)=>{if(!z(t))return"";if(e==="Bash"){let n=typeof t.stdout==="string"?t.stdout:"",r=typeof t.stderr==="string"?t.stderr:"";return[n,r].filter((s)=>s.length>0).join(`
|
|
5
|
+
`)}if(e==="Read"){if(typeof t.content==="string")return t.content;let{file:n}=t;return z(n)&&typeof n.content==="string"?n.content:""}return typeof t.content==="string"?t.content:""};import{appendFileSync as Kn,mkdirSync as Qn}from"fs";import{homedir as Xn}from"os";import Yn from"path";var xt=`${Xn()}/.claude/tripwire.log`;try{Qn(Yn.dirname(xt),{recursive:!0})}catch{}var A=(e,t)=>{let n=new Date().toISOString(),r=t instanceof Error?t.stack??t.message:String(t);try{Kn(xt,`[${n}] [${e}] ${r}
|
|
6
|
+
`)}catch{}};var er=new Set(["unsupported-shell-structure","source-script","rm-rf-root","rm-rf-home","fork-bomb","dd-raw-device","mkfs","kill-all","diskutil-destructive","tmutil-destructive","shutdown","csrutil","nvram","kextload","spctl-disable","xattr-quarantine-bypass","topgrade","softwareupdate-install","systemsetup","scutil-set","security-keychain-destructive","no-verify","no-gpg-sign","curl-pipe-shell","gh-api-mutation","gh-destructive","flyctl-destroy","gcloud-delete","git-dynamic-subcommand","git-dynamic-mutation","git-restore-discard","git-checkout-force","git-checkout-discard","git-checkout-discard-all","git-switch-force","git-reset-hard","git-force-push","git-filter","git-update-ref","git-reflog-expire","git-symbolic-ref-write"]),U=(e,t)=>{if(!e.hasBypass||t.kind==="allow"||er.has(t.rule))return t;return c(t.rule)};var tr=(e)=>e.tokens.slice(1).join(" "),Et=(e,...t)=>{let n=new Set(e.flags);return t.some((r)=>n.has(r))},nr=new Map([["launchctl",1],["defaults",1],["diskutil",1],["tmutil",1],["security",1],["brew",1],["mas",1],["kmutil",1],["gh",2]]),rr=new Set(["dd","kill","chmod","softwareupdate","pmset","dscl","xattr","spctl","systemsetup","scutil","flyctl","gcloud"]),sr=(e)=>{if(e.head==="dd")return e.words.slice(1).some((n)=>n.kind==="dynamic"&&!n.source.startsWith("if="));if(e.head==="rsync"){let n=e.words.findIndex((s)=>s.value==="--"),r=n===-1?e.words.length:n;return e.words.slice(1,r).some((s)=>s.kind==="dynamic")}if(e.head==="gh"&&e.tokens[1]==="api"){for(let n=2;n<e.words.length;n+=1){let r=e.words[n-1]?.value,s=e.words[n];if(s?.kind==="dynamic"&&(r==="--method"||r==="-X"||s.source.startsWith("-X")||s.source.startsWith("--method=")))return!0}return!1}let t=nr.get(e.head);if(t!==void 0)return e.words.slice(1,t+1).some((n)=>n.kind==="dynamic");return rr.has(e.head)&&e.words.slice(1).some((n)=>n.kind==="dynamic")},ir=(e)=>{if(e.head!=="gh"||e.tokens[1]!=="api")return null;for(let t=2;t<e.tokens.length;t+=1){let n=e.tokens[t];if(n==="--method"||n==="-X")return e.tokens[t+1]?.toUpperCase()??"";if(n?.startsWith("--method=")===!0)return n.slice(9).toUpperCase();if(n?.startsWith("-X")===!0&&n.length>2)return n.slice(2).toUpperCase()}return null},or=(e)=>{if(e.head!=="gh"||e.tokens[1]!=="api")return!1;let t=ir(e);if(t!==null&&["POST","PUT","PATCH","DELETE"].includes(t))return!0;return e.flags.some((n)=>n==="--input"||n.startsWith("--input=")||n==="-f"||n==="-F"||n==="--field"||n.startsWith("--field=")||n==="--raw-field"||n.startsWith("--raw-field="))},ar=[{rule:"rm-rf-root",action:"deny",message:"rm -rf / is catastrophic. If the goal is cleaning a subdirectory, scope the path inside the project (e.g. ./dist).",match:(e)=>e.head==="rm"&&Et(e,"-rf","-fr","-Rf","-fR")&&e.tokens.includes("/")},{rule:"rm-rf-home",action:"deny",message:"rm -rf on $HOME / ~ would erase the home directory. Refuse.",match:(e)=>e.head==="rm"&&Et(e,"-rf","-fr","-Rf","-fR")&&e.tokens.some((t)=>/^(?<home>~|\$HOME|\$\{HOME\})$/u.test(t))},{rule:"fork-bomb",action:"deny",message:"Fork bomb pattern detected. Refuse.",match:(e,t)=>/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/u.test(t)},{rule:"source-script",action:"deny",message:"`source` / `.` executes a file in the current shell. Refuse arbitrary sourced scripts.",match:(e)=>(e.head==="source"||e.head===".")&&e.tokens.length>1},{rule:"dd-raw-device",action:"deny",message:"dd writing to a raw block device wipes the disk. Refuse.",match:(e)=>e.head==="dd"&&/\bof=\/dev\/(?<type>disk|sd|nvme|rdisk)/iu.test(tr(e))},{rule:"mkfs",action:"deny",message:"mkfs formats a filesystem. Refuse.",match:(e)=>/^mkfs(?<ext>\.[a-z0-9]+)?$/iu.test(e.head)},{rule:"kill-all",action:"deny",message:"kill -9 -1 kills every process you own. Refuse.",match:(e)=>e.head==="kill"&&e.tokens.includes("-9")&&e.tokens.includes("-1")},{rule:"chmod-777-recursive",action:"deny",message:"Recursive chmod 777 makes everything world-writable. Use 755 for directories, 644 for files, scoped narrowly.",match:(e)=>e.head==="chmod"&&e.flags.some((t)=>t.includes("R"))&&e.tokens.includes("777")},{rule:"no-verify",action:"deny",message:"--no-verify skips git hooks. Per policy: never skip hooks. Fix the underlying issue.",match:(e)=>e.tokens.includes("--no-verify")},{rule:"no-gpg-sign",action:"deny",message:"Bypassing GPG signing is off-limits unless explicitly requested.",match:(e,t)=>/--no-gpg-sign\b|-c\s+commit\.gpgsign=false/u.test(t)},{rule:"sudo",action:"ask",message:"sudo escalates privileges and is almost never needed in a coding session. If genuinely required, explain why; otherwise find a non-sudo path.",match:(e)=>e.head==="sudo"},{rule:"shutdown",action:"deny",message:"shutdown / reboot / halt control the machine. Refuse \u2014 system control should be done manually.",match:(e)=>["shutdown","reboot","halt","poweroff"].includes(e.head)},{rule:"launchctl-mutation",action:"deny",message:"launchctl load/unload/bootstrap/bootout/kickstart mutates system services. Refuse \u2014 surface the intent instead.",match:(e)=>e.head==="launchctl"&&typeof e.tokens[1]==="string"&&["load","unload","bootstrap","bootout","kickstart","enable","disable"].includes(e.tokens[1])},{rule:"defaults-write",action:"deny",message:"`defaults write` mutates macOS preferences. Refuse \u2014 surface the intent.",match:(e)=>e.head==="defaults"&&e.tokens[1]==="write"},{rule:"csrutil",action:"deny",message:"csrutil controls System Integrity Protection. Refuse.",match:(e)=>e.head==="csrutil"},{rule:"nvram",action:"deny",message:"nvram modifies firmware variables. Refuse.",match:(e)=>e.head==="nvram"},{rule:"diskutil-destructive",action:"deny",message:"diskutil eraseDisk / reformat / partitionDisk wipes disks. Refuse.",match:(e)=>e.head==="diskutil"&&typeof e.tokens[1]==="string"&&["eraseDisk","reformat","partitionDisk","eraseVolume","secureErase"].includes(e.tokens[1])},{rule:"tmutil-destructive",action:"deny",message:"tmutil delete / disablelocal touches Time Machine. Refuse.",match:(e)=>e.head==="tmutil"&&typeof e.tokens[1]==="string"&&["delete","disablelocal"].includes(e.tokens[1])},{rule:"osascript",action:"ask",message:"osascript runs arbitrary AppleScript and can do almost anything (move files, send emails, drive apps). Confirm with the user what you want done before running it.",match:(e)=>e.head==="osascript"},{rule:"topgrade",action:"deny",message:"topgrade upgrades everything (brew, mas, npm globals, rust, mise). System upgrades should be done manually.",match:(e)=>e.head==="topgrade"},{rule:"rsync-delete",action:"deny",message:"rsync --delete removes files at the destination. High blast radius \u2014 surface the intent instead.",match:(e)=>e.head==="rsync"&&e.flags.includes("--delete")},{rule:"softwareupdate-install",action:"deny",message:"`softwareupdate --install / -i / -d` triggers macOS system updates. Refuse \u2014 system updates should be done manually.",match:(e)=>e.head==="softwareupdate"&&e.flags.some((t)=>t==="--install"||t==="-i"||t==="-d"||t.startsWith("--download"))},{rule:"pmset-write",action:"deny",message:"`pmset` (with arguments) writes power-management settings. Read-only `pmset -g` is fine; mutations need the user.",match:(e)=>e.head==="pmset"&&e.tokens.length>1&&!e.tokens.includes("-g")&&!e.tokens.includes("-G")},{rule:"dscl-mutate",action:"deny",message:"`dscl . -create / -delete / -append / -change / -merge` modifies the local directory service (your user account, groups, etc.). Refuse.",match:(e)=>e.head==="dscl"&&e.tokens.some((t)=>["-create","-delete","-append","-change","-merge","-passwd"].includes(t))},{rule:"xattr-quarantine-bypass",action:"deny",message:"`xattr -d com.apple.quarantine` removes Gatekeeper's quarantine bit \u2014 the macOS protection against running untrusted binaries. Refuse.",match:(e)=>e.head==="xattr"&&e.tokens.includes("-d")&&e.tokens.some((t)=>t.includes("com.apple.quarantine"))},{rule:"spctl-disable",action:"deny",message:"`spctl --master-disable` / `--global-disable` disables Gatekeeper system-wide. Refuse.",match:(e)=>e.head==="spctl"&&e.flags.some((t)=>t==="--master-disable"||t==="--global-disable"||t==="--disable")},{rule:"kextload",action:"deny",message:"Loading a kernel extension (`kextload`, `kmutil load`) is a system-level mutation. Refuse \u2014 the user handles this manually.",match:(e)=>e.head==="kextload"||e.head==="kextunload"||e.head==="kmutil"&&(e.tokens[1]==="load"||e.tokens[1]==="unload")},{rule:"security-keychain-destructive",action:"deny",message:"`security delete-keychain / delete-generic-password / delete-internet-password / set-keychain-settings` mutates your Keychain (where CLIs store their secrets). Refuse \u2014 Keychain should be managed manually.",match:(e)=>e.head==="security"&&typeof e.tokens[1]==="string"&&["delete-keychain","delete-generic-password","delete-internet-password","delete-certificate","delete-identity","set-keychain-settings","unlock-keychain","lock-keychain","create-keychain"].includes(e.tokens[1])},{rule:"security-keychain-add-write",action:"ask",message:"`security add-generic-password / add-internet-password / add-certificate` writes to your Keychain. Other tools manage their own entries \u2014 confirm this is the right path before adding anything else manually.",match:(e)=>e.head==="security"&&typeof e.tokens[1]==="string"&&["add-generic-password","add-internet-password","add-certificate"].includes(e.tokens[1])},{rule:"systemsetup",action:"deny",message:"`systemsetup -set...` writes machine-wide settings (timezone, sleep, network time, restart-on-power-failure). Refuse.",match:(e)=>e.head==="systemsetup"&&e.tokens.some((t)=>t.startsWith("-set"))},{rule:"scutil-set",action:"deny",message:"`scutil --set` writes system configuration (computer name, hostname, LocalHostName). Refuse \u2014 read-only `scutil --get` is fine.",match:(e)=>e.head==="scutil"&&e.tokens.includes("--set")},{rule:"brew-mutation",action:"ask",message:"`brew install/uninstall/upgrade/untap` modifies the machine globally. Confirm before running.",match:(e)=>e.head==="brew"&&typeof e.tokens[1]==="string"&&["install","uninstall","upgrade","reinstall","untap","tap"].includes(e.tokens[1])},{rule:"mas-mutation",action:"ask",message:"`mas install/uninstall` changes installed Mac App Store apps. Confirm.",match:(e)=>e.head==="mas"&&typeof e.tokens[1]==="string"&&["install","uninstall","purchase"].includes(e.tokens[1])},{rule:"gh-destructive",action:"deny",message:"gh repo/release/issue delete is destructive on shared state. Refuse.",match:(e)=>e.head==="gh"&&typeof e.tokens[1]==="string"&&typeof e.tokens[2]==="string"&&["repo","release","issue","pr"].includes(e.tokens[1])&&["delete","destroy","remove"].includes(e.tokens[2])},{rule:"gh-api-mutation",action:"deny",message:"`gh api` with a mutating HTTP method can change shared GitHub state and bypass Git workflow protections. Refuse the inline mutation.",match:or},{rule:"flyctl-destroy",action:"deny",message:"flyctl apps destroy / volumes destroy nukes deployed infra. Refuse.",match:(e)=>e.head==="flyctl"&&e.tokens.some((t)=>t==="destroy"||t==="delete")},{rule:"gcloud-delete",action:"deny",message:"`gcloud ... delete` affects cloud resources. Refuse.",match:(e)=>e.head==="gcloud"&&e.tokens.includes("delete")}],Rt=(e,t={})=>{let n=[];if(e.diagnostics.length>0){let r=[...new Set(e.diagnostics.map((i)=>i.message))],s=u("unsupported-shell-structure",`Tripwire cannot inspect this shell program safely: ${r.join(" ")}`);n.push(U(e,s))}for(let r of e.invocations)if(sr(r)){let s=u("unsupported-shell-structure",`Tripwire cannot classify computed policy arguments to \`${r.head}\` safely.`);n.push(U(e,s))}else{let s=ar.find((i)=>i.match(r,e.source));if(s!==void 0){let i=s.action==="deny"?u(s.rule,s.message):m(s.rule,s.message);n.push(U(e,lt(i,t)))}}return n.length===0?c("bash-deny"):L(n)};var Ie=(e)=>e.protectedBranches??[],lr=/^(?<type>feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?<scope>\([\w./\- ]+\))?!?:\s+\S/u,cr=new Set(["-C","-c","--git-dir","--work-tree","--namespace","--super-prefix"]),dr=new Set(["--bare","--paginate","-p","--no-pager","--no-replace-objects","--literal-pathspecs","--glob-pathspecs","--noglob-pathspecs","--icase-pathspecs","--no-optional-locks","--exec-path","--html-path","--man-path","--info-path"]),Pt=new Set(["status","diff","diff-tree","log","show","blame","rev-parse","rev-list","ls-files","ls-tree","cat-file","reflog","describe","shortlog","whatchanged","archive","bundle","fsck","fetch","ls-remote","help","version","grep","name-rev","merge-base","symbolic-ref","check-ignore","count-objects","verify-commit","verify-tag"]),ur=(e)=>{if(e.head!=="git")return null;let t=e.tokens.slice(1),n=0;while(n<t.length){let r=t[n];if(r===void 0)break;if(cr.has(r))n+=2;else if(dr.has(r))n+=1;else if(r.startsWith("--git-dir=")||r.startsWith("--work-tree=")||r.startsWith("--namespace=")||r.startsWith("--super-prefix=")||r.startsWith("--exec-path="))n+=1;else if(r.startsWith("-"))n+=1;else return{subcommand:r,subcommandIndex:n+1,subArgs:t.slice(n+1)}}return null},fr=(e)=>{for(let t=0;t<e.length;t+=1){let n=e[t];if(n===void 0)break;if(n==="-m"||n==="--message")return e[t+1]??null;if(n.startsWith("--message="))return n.slice(10);if(/^-[a-zA-Z]*m[a-zA-Z]*$/u.test(n))return e[t+1]??null}return null},pr=(e,t)=>{let n=Ie(t);for(let r of e)for(let s of n)if(r===s||r.endsWith(`:${s}`)||r.endsWith(`/${s}`))return s;return null},hr=(e)=>e.filter((t)=>!t.startsWith("-")),mr=(e)=>e.filter((t)=>t.startsWith("-")),h=(e,...t)=>{let n=new Set(e);return t.some((r)=>n.has(r))},gr=({subArgs:e,positional:t})=>{if(h(e,"--global","--system"))return u("git-config-global","Modifying global / system git config is off-limits \u2014 that is your personal identity. Read-only `git config --get` is fine.");if(!h(e,"--get","-l","--list","--get-all","--get-regexp")&&t.length>=2)return u("git-config-write","Local git config writes should be done explicitly. To read a value, use `git config --get <key>`.");return c("bash-git")},yr=({subArgs:e})=>{if(h(e,"--cached"))return c("bash-git");return m("git-rm","`git rm <path>` removes from the index AND the working tree. To untrack-only, use `git rm --cached <path>`. To delete the file separately, use `trash` / `rip`. Confirm intent.")},Sr=({subArgs:e,positional:t})=>{if(h(e,"--staged","-S")&&!h(e,"--worktree","-W"))return c("bash-git");if(t.length>0)return u("git-restore-discard","`git restore <path>` discards uncommitted changes in the working tree. Refuse \u2014 `git diff <path>` to inspect first, or `git stash push <path>` to preserve.");return c("bash-git")},br=({subArgs:e,positional:t})=>{if(h(e,"-b","-B"))return c("bash-git");if(h(e,"-f","--force"))return u("git-checkout-force","`git checkout -f` overwrites the working tree without preserving uncommitted changes. Refuse.");if(e.includes("--"))return u("git-checkout-discard","`git checkout -- <path>` discards uncommitted working-tree changes. Refuse \u2014 use `git stash push <path>` to preserve, or `git diff <path>` to inspect first.");let[n]=t;if(n!==void 0&&t.length===1&&(n==="."||n.startsWith("./")))return u("git-checkout-discard-all","`git checkout .` discards ALL uncommitted working-tree changes. Refuse \u2014 `git stash` to preserve, or `git diff` to inspect first.");return c("bash-git")},kr=({subArgs:e})=>{if(h(e,"-f","--force","--discard-changes"))return u("git-switch-force","`git switch -f / --discard-changes` throws away uncommitted working-tree changes. Refuse.");return c("bash-git")},wr=({subArgs:e,flags:t,positional:n})=>{if(h(e,"--hard"))return u("git-reset-hard","`git reset --hard` discards all uncommitted changes AND moves HEAD. Refuse \u2014 describe the intent in chat. If undoing a published commit, `git revert <sha>` is safer.");if(h(e,"--keep"))return m("git-reset-keep","`git reset --keep` resets HEAD but preserves uncommitted local changes. Confirm intent.");if(n.length===0&&t.length===0)return c("bash-git");return m("git-reset-mixed","`git reset` moves HEAD. Confirm intent \u2014 if undoing a commit, `git revert` is usually safer.")},vr=({subArgs:e,flags:t})=>{if(h(e,"-n","--dry-run")||t.some((n)=>/^-[a-zA-Z]*n/u.test(n)))return c("bash-git");if(t.some((n)=>/^-[a-zA-Z]*[df]/u.test(n)||n==="--force"))return u("git-clean-fd","`git clean -fd` deletes untracked files (often your in-progress work). Refuse \u2014 inspect with `git clean -dn` (dry run) first. If genuinely needed, append ` # tripwire-allow: <reason>`.");return c("bash-git")},xr=({subArgs:e,positional:t,config:n})=>{if(h(e,"--abort","--quit","--continue","--skip","--edit-todo"))return c("bash-git");if(h(e,"-i","--interactive"))return u("git-rebase-interactive","`git rebase -i` rewrites history interactively. Refuse \u2014 too easy to lose commits in the agent loop. If this is genuinely required, do it manually outside the agent.");let[r]=t,s=Ie(n);if(r!==void 0&&s.includes(r))return m("git-rebase-onto-protected",`Rebasing onto \`${r}\` rewrites history of the current branch. \`git merge ${r}\` is usually safer. Confirm intent.`);return m("git-rebase","`git rebase` rewrites commit history. `git merge` is usually safer. Confirm intent.")},Er=({subArgs:e})=>{if(h(e,"--abort","--quit","--continue","--skip"))return c("bash-git");return m("git-cherry-pick","`git cherry-pick` applies commits onto the current branch and can create conflicts. Confirm intent.")},Rr=({subArgs:e})=>{if(h(e,"--abort","--continue","--quit"))return c("bash-git");return m("git-merge","`git merge <branch>` may create merge conflicts. Confirm intent.")},Cr=({subArgs:e,config:t})=>{if(h(e,"--amend"))return u("git-commit-amend","`git commit --amend` rewrites the last commit. If it has been pushed, this causes upstream divergence. Refuse \u2014 surface the intent.");let n=fr(e),r=h(e,"-F","--file","-c","-C","--reuse-message","--reedit-message"),s=h(e,"--no-edit");if(n===null&&!r&&!s)return u("git-commit-no-message",'`git commit` without `-m "..."` opens an editor and hangs the agent. Use `git commit -m "<conventional message>"`.');if(n!==null&&t.enforceConventionalCommits===!0&&!lr.test(n))return u("git-commit-non-conventional",["Commit message must follow Conventional Commits format:"," `<type>(<scope>)?(!)?: <description>`","Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.","Examples:"," `fix(auth): handle expired token refresh`"," `feat: add bash-git rule`"," `chore: bump deps`",`Got: ${JSON.stringify(n)}`].join(`
|
|
7
|
+
`));if(h(e,"-a","--all")||e.some((i)=>/^-[a-zA-Z]*a[a-zA-Z]*$/u.test(i)))return m("git-commit-auto-stage","`git commit -a / --all` auto-stages every tracked change. Explicit `git add <files>` first is usually clearer about what is being committed. Confirm.");return c("bash-git")},Pr=({subArgs:e,flags:t,positional:n,config:r})=>{if(t.some((i)=>i==="--force"||i==="-f"||i.startsWith("--force-with-lease")))return u("git-force-push","Force push is forbidden. If a branch needs to be reset upstream, surface the intent \u2014 there is almost always a non-force path.");if(h(e,"--delete","--mirror")||e.some((i)=>i.startsWith(":")))return u("git-push-delete","Refusing to delete a remote branch via push. If genuinely needed, surface the intent.");let s=pr(n,r);if(s!==null)return u("git-push-protected",`Refusing to push directly to protected branch \`${s}\`. Open a PR instead: \`gh pr create\`.`);return c("bash-git")},Ar=({subArgs:e,flags:t,positional:n,config:r})=>{let s=t.find((i)=>i==="-D"||i==="-d"||i==="--delete"||/^-[a-zA-Z]*D/u.test(i)||/^-[a-zA-Z]*d/u.test(i));if(s!==void 0){let i=n,o=new Set(Ie(r)),a=i.find((l)=>o.has(l));if(a!==void 0)return u("git-branch-delete-protected",`Refusing to delete protected branch \`${a}\`.`);if(s==="-D"||s.includes("D"))return u("git-branch-force-delete",`\`git branch -D ${i.join(" ")}\` force-deletes branches even if unmerged (potential data loss). To delete a merged branch, use \`-d\`. To force, append \` # tripwire-allow: <reason>\`.`);return m("git-branch-delete",`\`git branch -d ${i.join(" ")}\` deletes a branch (merged-only check). Confirm intent.`)}if(h(e,"-m","-M","--move"))return m("git-branch-move","Renaming a branch can confuse pushed remotes. Confirm intent.");return c("bash-git")},_r=({subArgs:e})=>{if(h(e,"-d","--delete"))return u("git-tag-delete","`git tag -d` deletes a tag. Refuse \u2014 surface intent.");return c("bash-git")},Ir=({subArgs:e})=>{let t=e[0]??"push";if(t==="drop"||t==="clear")return u("git-stash-drop",`\`git stash ${t}\` discards stashed work. Refuse \u2014 \`git stash list\` and \`git stash show\` to inspect first.`);return c("bash-git")},Wr=({flags:e})=>{if(e.some((t)=>t.startsWith("--prune=")||t==="--aggressive"))return u("git-gc-prune","`git gc --prune=now` / `--aggressive` destroys reflog recovery options. Refuse.");return c("bash-git")},Tr=({subArgs:e})=>{let[t]=e;if(t!==void 0&&["add","prune","remove","rename","rm","set-branches","set-head","set-url","update"].includes(t))return m("git-remote-mutate",`\`git remote ${t}\` changes which remote you're pushing to. Confirm \u2014 accidentally pointing at the wrong remote is high blast-radius.`);return c("bash-git")},Dr=({subArgs:e})=>{if(h(e,"-f","--force"))return u("git-checkout-index-force","`git checkout-index --force` can overwrite working-tree files. Refuse.");return m("git-checkout-index","`git checkout-index` writes files from the index into the working tree. Confirm the target and intent.")},Ct=({subcommand:e,subArgs:t})=>{let n=t[0]??"";if(["add","remove","rm","deinit","sync","set-url"].includes(n))return m("git-submodule-worktree-mutate",`\`git ${e} ${n}\` modifies repo structure. Confirm intent.`);return c("bash-git")},Hr=new Map([["config",gr],["add",()=>c("bash-git")],["mv",()=>c("bash-git")],["rm",yr],["restore",Sr],["checkout",br],["switch",kr],["reset",wr],["clean",vr],["rebase",xr],["cherry-pick",Er],["merge",Rr],["commit",Cr],["push",Pr],["branch",Ar],["tag",_r],["stash",Ir],["filter-branch",({subcommand:e})=>u("git-filter",`\`git ${e}\` rewrites entire repo history. Refuse.`)],["filter-repo",({subcommand:e})=>u("git-filter",`\`git ${e}\` rewrites entire repo history. Refuse.`)],["gc",Wr],["update-ref",()=>u("git-update-ref","`git update-ref` directly mutates refs and bypasses normal git operations. Refuse.")],["remote",Tr],["submodule",Ct],["worktree",Ct],["checkout-index",Dr],["write-tree",()=>c("bash-git")],["init",({subcommand:e})=>E(`git-${e}`,`\`git ${e}\` is allowed but unusual mid-session. Make sure this is what the user asked for.`)],["clone",({subcommand:e})=>E(`git-${e}`,`\`git ${e}\` is allowed but unusual mid-session. Make sure this is what the user asked for.`)]]),Fr=(e,t)=>{let{subcommand:n,subArgs:r}=e,s=mr(r),i=hr(r);if(Pt.has(n)){if(n==="reflog"&&(r[0]==="expire"||h(r,"--expire")))return u("git-reflog-expire","`git reflog expire` destroys git's recovery history. Refuse \u2014 surface the intent.");if(n==="symbolic-ref"&&i.length>=2)return u("git-symbolic-ref-write","`git symbolic-ref <name> <ref>` rewrites a symbolic ref. Refuse.");return c("bash-git")}let o=Hr.get(n);if(o!==void 0)return o({subcommand:n,subArgs:r,flags:s,positional:i,config:t});return E("git-unknown-subcommand",`\`git ${n}\` is not classified by tripwire. Allowing \u2014 flag if this looks like history-rewriting or data-loss territory.`)},$r=(e)=>{if(Pt.has(e.subcommand))return!0;if(e.subcommand!=="remote")return!1;let t=e.subArgs.find((n)=>!n.startsWith("-"));return t===void 0||t==="get-url"||t==="show"},At=(e,t)=>{for(let n of e.invocations){let r=ur(n);if(r!==null){if(n.words[r.subcommandIndex]?.kind==="dynamic")return u("git-dynamic-subcommand","Tripwire cannot classify a Git subcommand that is computed at runtime.");if((!$r(r)||r.subcommand==="reflog"||r.subcommand==="symbolic-ref")&&n.words.slice(1).some((o)=>o.kind==="dynamic"))return u("git-dynamic-mutation",`Tripwire cannot prove that computed arguments to \`git ${r.subcommand}\` preserve Git safety policy.`);let i=Fr(r,t);if(i!==null&&i.kind!=="allow")return i}}return c("bash-git")};var Or=new Set(["curl","wget","wget2","aria2c","xh"]),Br=new Set(["bash","sh","zsh","fish"]),Lr=(e)=>{for(let t of e.invocations){let{pipeline:n}=t;if(Or.has(t.head)&&n!==void 0){if(e.invocations.find((s)=>s.pipeline?.id===n.id&&s.pipeline.index===n.index+1&&Br.has(s.head))!==void 0)return!0}}return!1},Mr=[{head:"cargo",subcommand:"install",rule:"cargo-install",message:"Confirm before `cargo install <crate>`: this builds and installs arbitrary code from crates.io into ~/.cargo/bin globally."},{head:"go",subcommand:"install",rule:"go-install",message:"Confirm before `go install`: this fetches and installs arbitrary Go code globally."},{head:"gem",subcommand:"install",rule:"gem-install",message:"Confirm before `gem install`: pulls arbitrary code from rubygems.org."}],_t=(e)=>{if(Lr(e))return u("curl-pipe-shell","Piping `curl` / `wget` directly into a shell runs whatever the remote URL serves. Refuse \u2014 download to a file, inspect, then run if appropriate. If you genuinely need this, append ` # tripwire-allow: <reason>` (and explain to the user what you're running).");for(let t of e.invocations)for(let n of Mr)if(t.head===n.head&&t.tokens[1]===n.subcommand)return m(n.rule,n.message);return c("bash-network-install")};import{lstatSync as Nr,readlinkSync as zr,realpathSync as Ur}from"fs";import V from"path";var Vr=[{pattern:/(?<prefix>^|\/)\.env(?<ext>\.[^/]+)?$/u,rule:"env-file",message:".env files hold secrets that should never be sent to the model. Refuse to write or edit. If an example is needed, create .env.example with redacted placeholders."},{pattern:/(?<prefix>^|\/)\.dev\.vars(?<ext>\.[^/]+)?$/u,rule:"dev-vars",message:".dev.vars holds Cloudflare/Wrangler secrets. Do not modify."},{pattern:/(?<prefix>^|\/)\.ssh\//u,rule:"ssh-dir",message:"Never write into ~/.ssh/. Refuse."},{pattern:/(?<prefix>^|\/)(?<key>id_rsa|id_ed25519|id_ecdsa|id_dsa)(?<pub>\.pub)?$/u,rule:"ssh-key",message:"SSH key file. Refuse."},{pattern:/\.(?<ext>pem|key|p12|pfx)$/iu,rule:"private-key",message:"Private key file. Refuse to overwrite. If generating a new key, use a different filename and let the user review."},{pattern:/(?<prefix>^|\/)secrets?\.(?<ext>json|ya?ml|toml|env)$/iu,rule:"secrets-file",message:"Secrets file. Refuse."},{pattern:/(?<prefix>^|\/)\.aws\/credentials$/u,rule:"aws-credentials",message:"AWS credentials file. Refuse."},{pattern:/(?<prefix>^|\/)\.netrc$/u,rule:"netrc",message:".netrc holds host credentials. Refuse."}],It=(e)=>{try{return Ur(e)}catch{return null}},We=(e,t=new Set)=>{if(t.has(e))return e;t.add(e);let n=It(e);if(n!==null)return n;try{if(Nr(e).isSymbolicLink()){let s=zr(e);return We(V.resolve(V.dirname(e),s),t)}}catch{}let r=V.dirname(e);if(r===e)return e;return V.join(We(r,t),V.basename(e))},Z=(e,t,n)=>{let r=V.resolve(e),s=t==="write"?We(r):It(r),i=[e,r];if(s!==null&&!i.includes(s))i.push(s);for(let o of i)for(let a of n)if(a.pattern.test(o))return a;return null},Te=(e)=>{let t=Z(e.file_path,"write",Vr);if(t!==null)return u(t.rule,t.message);return c("path-protect")};var Gr=[{rule:"redirect-env",pattern:/(?<prefix>^|\/)\.env(?<ext>\.[^/]+)?$/u,message:"Refusing to write into a .env file via shell redirect / tee / cp / mv. .env files hold secrets \u2014 never overwrite from a tool call."},{rule:"redirect-dev-vars",pattern:/(?<prefix>^|\/)\.dev\.vars(?<ext>\.[^/]+)?$/u,message:"Refusing to write into .dev.vars (Cloudflare/Wrangler secrets)."},{rule:"redirect-ssh",pattern:/(?<prefix>^|\/)\.ssh\//u,message:"Refusing to write into ~/.ssh/ via shell."},{rule:"redirect-key",pattern:/\.(?<ext>pem|key|p12|pfx)$/iu,message:"Refusing to overwrite a private-key-shaped file via shell."},{rule:"redirect-aws-credentials",pattern:/(?<prefix>^|\/)\.aws\/credentials$/u,message:"Refusing to write into ~/.aws/credentials via shell."},{rule:"redirect-netrc",pattern:/(?<prefix>^|\/)\.netrc$/u,message:"Refusing to write into ~/.netrc via shell."},{rule:"redirect-block-device",pattern:/^\/dev\/(?<type>sd|disk|nvme|rdisk)/iu,message:"Redirecting into a raw block device wipes the disk. Refuse."}],jr=(e)=>{let t=Z(e,"write",Gr);if(t!==null)return u(t.rule,t.message);return null},De=(e)=>e.kind==="dynamic"||e.kind==="background-pid"?u("redirect-dynamic-target","Tripwire cannot prove that this computed write target avoids protected files."):jr(e.value),Wt=(e)=>{for(let t of e.redirects)if(t.op===">"||t.op===">>"||t.op==="&>"||t.op==="&>>"){let n=De(t.target);if(n!==null)return n}for(let t of e.invocations){let n=t.words.slice(1).filter((r)=>!r.value.startsWith("-"));if(t.head==="tee")for(let r of n){let s=De(r);if(s!==null)return s}if(t.head==="cp"||t.head==="mv"){let r=n.at(-1);if(r!==void 0){let s=De(r);if(s!==null)return s}}}return c("bash-redirect")};var qr=(e,t)=>{let n=!1,r=[];for(let o of e.tokens.slice(1))if(!n&&o==="--")n=!0;else if(n||!o.startsWith("-")||o==="-")r.push(o);let s=t.relative??[],i=t.absolute??[];return r.filter((o)=>!ve(o,s,i))},Jr=(e,t)=>{if(!e.tokens.includes("-delete"))return null;let n=[];for(let o of e.tokens.slice(1)){if(o.startsWith("-"))break;n.push(o)}let r=n.length===0?["."]:n,s=t.relative??[],i=t.absolute??[];return r.filter((o)=>!ve(o,s,i))},Tt=(e,t)=>{let n=[];for(let o of e.invocations)if(o.head==="rm"){let a=qr(o,t);if(a.length>0)n.push({kind:"rm",targets:a})}else if(o.head==="find"){let a=Jr(o,t);if(a!==null&&a.length>0)n.push({kind:"find -delete",targets:a})}if(n.length===0)return c("bash-scoped-rm");let r=t.relative??[],s=t.absolute??[],i=n.map((o)=>` \u2022 ${o.kind} on: ${o.targets.map((a)=>JSON.stringify(a)).join(", ")}`).join(`
|
|
8
|
+
`);return u("destructive-outside-safe-paths",`Destructive deletion outside known-safe scopes is blocked. Use a recoverable deletion tool or limit the target to an ephemeral build, cache, state, or temporary directory:
|
|
9
|
+
${ot(r,s)}
|
|
16
10
|
|
|
17
11
|
Flagged targets:
|
|
18
12
|
${i}
|
|
19
13
|
|
|
20
|
-
If raw deletion is genuinely needed, append \` # tripwire-allow: <reason>\` to the command.`)};var
|
|
14
|
+
If raw deletion is genuinely needed, append \` # tripwire-allow: <reason>\` to the command.`)};var Zr=(e)=>e==="--extract"||e.startsWith("-")&&!e.startsWith("--")&&e.slice(1).includes("x"),Kr=(e)=>{for(let t=0;t<e.tokens.length;t+=1){let n=e.tokens[t];if(n!==void 0){if(n==="-C"||n==="--directory")return e.tokens[t+1]??null;if(n.startsWith("--directory="))return n.slice(12)}}return null},Dt=(e)=>e==="/"||/^(?<home>~|\$HOME|\$\{HOME\})$/u.test(e),Qr=(e)=>{let t=e.tokens.indexOf("-d");return t===-1?void 0:e.tokens[t+1]},Ht=(e)=>{for(let t of e.invocations)if(t.head==="tar"){let[,n]=t.tokens,s=t.flags.some(Zr)||t.tokens.includes("--extract")||n!==void 0&&/^[a-zA-Z]+$/u.test(n)&&n.includes("x")?Kr(t):null;if(s!==null&&Dt(s))return u("tar-extract-to-root",`tar -x with -C ${s} can overwrite arbitrary system files. Refuse \u2014 extract to a contained directory (e.g. ./tmp/extract) and inspect before moving anything elsewhere.`)}for(let t of e.invocations)if(t.head==="unzip"){let n=Qr(t);if(n!==void 0&&Dt(n))return u("unzip-to-root",`unzip -d ${n} can overwrite arbitrary system files. Refuse \u2014 extract to a contained directory.`)}return c("bash-tar-explosion")};var Xr="If this is intentional, append ` # tripwire-allow: <reason>` to the command.",Yr=new Map([["add","create"],["new","create"],["edit","update"],["set","update"],["rm","delete"],["del","delete"],["remove","delete"]]),Ft=(e)=>Yr.get(e)??e,$t=(e)=>{let t=e.lastIndexOf("/");return t===-1?e:e.slice(t+1)},es=(e,t)=>e.some((n)=>n===t||n.startsWith(`${t}=`)),ts=(e,t)=>{for(let n=0;n<e.length;n+=1){let r=e[n];if(r!==void 0){if(r===t)return e[n+1]??"";if(r.startsWith(`${t}=`))return r.slice(t.length+1)}}return null},ns=(e)=>{let t=[],n=e.tokens.slice(1);for(let r=0;r<n.length;r+=1){let s=n[r];if(s?.startsWith("-")===!0){let i=n[r+1];if(!s.includes("=")&&i!==void 0&&!i.startsWith("-"))r+=1}else if(s!==void 0)t.push(s)}return t},Ot=(e,t)=>{let{pattern:n}=t,r=j(n);if(r.diagnostics.length>0||r.invocations.length===0)return!1;let[s]=r.invocations;if(s===void 0)return!1;let[i,...o]=s.tokens;if(i===void 0)return!1;return e.invocations.some((a)=>{if($t(a.head)!==$t(i))return!1;let l=ns(a),f=o.every((v,H)=>l[H]!==void 0&&Ft(l[H])===Ft(v)),p=(t.requiresFlags??[]).every((v)=>es(a.tokens,v)),C=(t.forbidsFlagValues??[]).every((v)=>{let H=ts(a.tokens,v.flag);return H!==null&&v.values.includes(H)});return f&&p&&C})},Bt=(e,t,n)=>{for(let r of n)if(Ot(e,r))return c("config-custom");for(let r of t)if(Ot(e,r)){let s=r.message.includes("tripwire-allow")?r.message:`${r.message} ${Xr}`;return r.action==="ask"?m("config-custom",s):u("config-custom",s)}return c("config-custom")};import{readFileSync as rs}from"fs";var Lt=(e,t)=>{let n=new Set(e.split(`
|
|
21
15
|
`).map((r)=>r.trim()).filter((r)=>r.length>0));return t.split(`
|
|
22
|
-
`).filter((r)=>r.trim().length>0).filter((r)=>!n.has(r.trim()))},
|
|
23
|
-
`),
|
|
24
|
-
\u2026and ${
|
|
25
|
-
`))};import{spawnSync as
|
|
16
|
+
`).filter((r)=>r.trim().length>0).filter((r)=>!n.has(r.trim()))},Mt=(e)=>{try{return rs(e,"utf8")}catch(t){if(t instanceof Error&&"code"in t&&t.code==="ENOENT")return"";throw t}};var ss=[/\bTODO\s*:/iu,/\bFIXME\s*:/iu,/\bXXX\s*:/iu,/\bHACK\s*:/iu,/\bfor now\b/iu,/\bnot implemented\b/iu,/\bNotImplementedError\b/u,/\btemp fix\b/iu,/\bfallback\b/iu,/\bplaceholder\b/iu,/\bbackwards?[ -]?compat(?<ibility>ibility)?\b/iu,/\bfor later\b/iu,/\blater on\b/iu,/\bget back to\b/iu,/\bI'?ll fix\b/iu,/\bto be implemented\b/iu,/\bnot yet (?<state>implemented|done)\b/iu,/\bstubbed\b/iu],is=/\.(?<ext>ts|tsx|js|jsx|mjs|cjs|py|rs|go|rb|java|kt|swift|c|cc|cpp|h|hpp|cs|php|sh|zsh|bash|lua|ex|exs|clj|scala|dart)$/iu,os=/(?<prefix>^|\/)(?<dir>__tests__|tests?|spec|fixtures?|mocks?|__mocks__|stories)(?<suffix>\/|$)|\.(?<ext>test|spec|fixture|mock|stories)\.[^/]+$/iu,as=/tripwire-allow:[ \t]*\S/u,ls=(e)=>{if(as.test(e))return!1;for(let t of ss)if(t.test(e))return!0;return!1},He=(e)=>{let t=e.file_path;if(!is.test(t)||os.test(t))return c("lazy-code");let n="content"in e?e.content:e.new_string,r="content"in e?Mt(t):e.old_string,s=[];for(let a of Lt(r,n))if(ls(a))s.push(a.slice(0,200));if(s.length===0)return c("lazy-code");let i=s.slice(0,3).map((a)=>` \u2022 ${a}`).join(`
|
|
17
|
+
`),o=s.length>3?`
|
|
18
|
+
\u2026and ${s.length-3} more`:"";return E("lazy-code-marker",["Heads up: line(s) you just added contain words that often signal incomplete or deferred work. The write went through \u2014 this is a flag, not a block.","",'Why this exists: AI coding agents have a strong pull toward stubbing things, deferring "for now," and shipping half-built fallbacks instead of finishing the work in the same turn. The point of this warning is to push back on that pull on every iteration. If you stubbed something out for time, finish it this turn rather than leaving deferred work for later.',"",'If the marker is genuinely permanent \u2014 a real product term ("auth fallback flow", "retry fallback chain", an HTML input placeholder, a public API field literally named "placeholder"), a logging tag, or a comment intentionally left for a human reader \u2014 no action needed. To silence the flag on subsequent edits of that line, append `tripwire-allow: <one-line reason>` (any comment syntax: `//`, `#`, `--`, etc.).',"","Flagged additions:",i+o].join(`
|
|
19
|
+
`))};import{spawnSync as cs}from"child_process";import{Result as ds,Schema as I}from"effect";var us=I.Struct({RuleID:I.String,Description:I.String,StartLine:I.Finite,EndLine:I.Finite,Secret:I.String,Match:I.String}),fs=I.Array(us),ps=["stdin","--report-format","json","--report-path","-","--exit-code","0","--no-banner","--no-color","--log-level","error"],hs=({executable:e,args:t,input:n,timeoutMs:r})=>{let s=cs(e,t,{encoding:"utf8",input:n,maxBuffer:67108864,stdio:["pipe","pipe","ignore"],timeout:r}),i={status:s.status,stdout:typeof s.stdout==="string"?s.stdout:""};return s.error===void 0?i:{...i,error:s.error}},ms=(e)=>{if(typeof e!=="object"||e===null||!("code"in e))return;return typeof e.code==="string"?e.code:void 0},gs=(e)=>{let t=ms(e);if(t==="ENOENT")return"missing-executable";if(t==="ETIMEDOUT")return"timeout";return"non-zero-exit"},ys=(e)=>{let t=new Map;for(let n of e)t.set(n.RuleID,(t.get(n.RuleID)??0)+1);return[...t.entries()].map(([n,r])=>({rule:n,count:r}))};var Ss=(e,t)=>{let n=e,r=[...t].toSorted((s,i)=>i.Secret.length-s.Secret.length);for(let s of r)if(s.Secret!=="")n=n.replaceAll(s.Secret,`[REDACTED:${s.RuleID}]`);return n},Nt=(e,t,n=hs)=>{if(e.length===0)return{ok:!0,hits:[],redacted:e};let r;try{r=n({executable:t.executable,args:ps,input:e,timeoutMs:t.timeoutMs})}catch{return{ok:!1,category:"non-zero-exit"}}if(r.error!==void 0)return{ok:!1,category:gs(r.error)};if(r.status!==0)return{ok:!1,category:"non-zero-exit"};let s;try{s=JSON.parse(r.stdout)}catch{return{ok:!1,category:"malformed-json"}}let i=I.decodeUnknownResult(fs)(s);if(ds.isFailure(i))return{ok:!1,category:"malformed-json"};let o=i.success;return{ok:!0,hits:ys(o),redacted:Ss(e,o)}};var bs=(e)=>`Tripwire could not verify this tool output because the configured secret scanner failed (${e}). The original output was withheld. Install Betterleaks 1.5.0 or later, check secretScanner.executable and secretScanner.timeoutMs, then run the original tool again.`,zt=(e)=>{let t=vt(e.toolName,e.response);if(t.length===0)return c("post-secret-scrub");let n=Nt(t,e.secretScanner,e.scannerRunner);if(!n.ok)return u("secret-scanner-failed",bs(n.category));if(n.hits.length===0)return c("post-secret-scrub");let r=n.hits.map((s)=>`${s.rule}\xD7${s.count}`).join(", ");return u("secrets-in-output",[`tripwire intercepted ${n.hits.length} secret pattern(s) in this tool's output (${r}). The original output was withheld so the secret never enters the model context. A redacted form is below \u2014 work from this, do not re-run the same command in a way that re-fetches the underlying secret.`,"","Redacted output:",n.redacted.slice(0,16000)+(n.redacted.length>16000?`
|
|
26
20
|
\u2026[truncated]`:"")].join(`
|
|
27
|
-
`))};var
|
|
21
|
+
`))};var ks=[{rule:"read-env",pattern:/(?<prefix>^|\/)\.env(?<ext>\.[^/]+)?$/u,message:".env files hold secrets that should never enter the model context. Refuse to read. If the goal is documenting required env vars, look at .env.example or describe the schema from memory."},{rule:"read-dev-vars",pattern:/(?<prefix>^|\/)\.dev\.vars(?<ext>\.[^/]+)?$/u,message:"Refuse to read .dev.vars (Cloudflare/Wrangler secrets)."},{rule:"read-ssh",pattern:/(?<prefix>^|\/)\.ssh\//u,message:"Refuse to read files inside ~/.ssh/."},{rule:"read-ssh-key",pattern:/(?<prefix>^|\/)(?<key>id_rsa|id_ed25519|id_ecdsa|id_dsa)$/u,message:"Refuse to read SSH private key files."},{rule:"read-private-key",pattern:/\.(?<ext>pem|key|p12|pfx)$/iu,message:"Refuse to read private-key-shaped files."},{rule:"read-aws-credentials",pattern:/(?<prefix>^|\/)\.aws\/credentials$/u,message:"Refuse to read ~/.aws/credentials."},{rule:"read-netrc",pattern:/(?<prefix>^|\/)\.netrc$/u,message:"Refuse to read ~/.netrc (host credentials)."},{rule:"read-secrets-file",pattern:/(?<prefix>^|\/)secrets?\.(?<ext>json|ya?ml|toml|env)$/iu,message:"Refuse to read a file named secrets.{json,yaml,toml,env}."}],Ut=(e)=>{let t=Z(e.file_path,"read",ks);if(t!==null)return u(t.rule,t.message);return c("read-protect")};var ws=(e,t)=>e.some((n)=>n.startsWith("-")&&!n.startsWith("--")&&n.slice(1).includes(t)),vs=(e,t)=>{if(!t.executables.includes(e.head))return!1;let n=e.tokens.slice(1),r=new Set(n);if(!(t.match?.argumentsIncludeAll??[]).every((s)=>r.has(s)))return!1;if(!(t.match?.argumentsStartWith??[]).every((s,i)=>n[i]===s))return!1;return(t.match?.shortFlagsIncludeAll??[]).every((s)=>ws(e.flags,s))},Vt=(e,t)=>{for(let n of e.invocations)for(let r of t)if(vs(n,r))return r.action==="deny"?u(r.rule,r.message):E(r.rule,r.message);return c("tool-policy")};var xs=async()=>{let e=[];for await(let t of process.stdin){let n=t;if(n instanceof Uint8Array)e.push(Buffer.from(n));else throw TypeError("Tripwire received a non-byte stdin chunk")}return Buffer.concat(e).toString("utf8")},Be={kind:"native"},Gt=250,Es="--tripwire-hook",Rs=ie.Array(_e).check(ie.isMinLength(1)),G=(e=Be)=>{if(e.kind==="cursor"&&!e.post){process.stdout.write(`{"continue":true,"permission":"allow"}
|
|
28
22
|
`);return}process.stdout.write(`{"continue": true}
|
|
29
|
-
`)},
|
|
30
|
-
`)},
|
|
31
|
-
`);return}let r=e.hook_event_name,
|
|
32
|
-
`);return}let
|
|
33
|
-
`)}
|
|
34
|
-
`)},
|
|
35
|
-
`)},
|
|
23
|
+
`)},Fe=(e)=>`[tripwire:${e.rule}] ${e.message}`,Qt=(e)=>{let t=e.kind==="ask"?`${Fe(e)} This unattended Cursor run cannot ask a human, so the action is denied.`:Fe(e);process.stdout.write(`${JSON.stringify({continue:!0,permission:"deny",user_message:t,agent_message:t})}
|
|
24
|
+
`)},Xt=(e)=>e.turn_id!==void 0,jt=(e,t,n)=>{if(n.kind==="cursor"){process.stdout.write(`${JSON.stringify({continue:!0,permission:"allow",agent_message:Fe(t)})}
|
|
25
|
+
`);return}let r=e.hook_event_name,s=`[tripwire:${t.rule}] ${t.message}`;if(Xt(e)){process.stdout.write(`${JSON.stringify({continue:!0,systemMessage:s})}
|
|
26
|
+
`);return}let i={hookEventName:r,additionalContext:s};process.stdout.write(`${JSON.stringify({continue:!0,hookSpecificOutput:i})}
|
|
27
|
+
`)},$e=(e,t,n=Be)=>{if(n.kind==="cursor"){Qt(t);return}let r={hookSpecificOutput:{hookEventName:e,permissionDecision:t.kind==="deny"?"deny":"ask",permissionDecisionReason:`[tripwire:${t.rule}] ${t.message}`}};process.stdout.write(`${JSON.stringify(r)}
|
|
28
|
+
`)},Cs=(e,t)=>{if(t.kind==="cursor"){G(t);return}let n={continue:!0,decision:"block",reason:`[tripwire:${e.rule}] ${e.message}`};process.stdout.write(`${JSON.stringify(n)}
|
|
29
|
+
`)},oe=(e)=>{let t=e.toLowerCase();if(t==="bash"||t==="exec"||t==="shell"||t==="run_command")return"Bash";if(t==="read"||t==="read_file")return"Read";if(t==="write"||t==="write_file")return"Write";if(t==="edit"||t==="edit_file"||t==="multiedit"||t==="apply_patch")return"Edit";if(t==="webfetch"||t==="web_fetch"||t==="fetch")return"WebFetch";if(t==="powershell")return"PowerShell";return e};class Yt extends Kt.TaggedError("RuleExecutionError"){}class en extends Kt.TaggedError("HookInputParseError"){}var Ps=(e,t,n)=>w.gen(function*(){let s=yield*w.exit(w.try({try:t,catch:(i)=>new Yt({cause:i})}).pipe(w.timeout(n)));if(se.isSuccess(s))return s.value;return A(e,K.pretty(s.cause)),c(e)}),tn=(e,t,n={})=>{let r=[],s=j(e,{...n,executionCarrierAliases:t.shell.executionCarrierAliases}),i=(o,a)=>({name:o,fn:()=>U(s,a())});return r.push(i("bash-deny",()=>Rt(s,t.ruleActions)),i("bash-git",()=>At(s,t.git)),i("bash-scoped-rm",()=>Tt(s,t.safePaths)),i("bash-redirect",()=>Wt(s)),i("bash-network-install",()=>_t(s)),i("bash-tar-explosion",()=>Ht(s)),i("tool-policy",()=>Vt(s,t.toolPolicies)),i("config-custom",()=>Bt(s,t.blockedCommands,t.allowedCommands))),r},nn=(e,t,n,r)=>{let s=[];if(e==="PowerShell")return s.push({name:"powershell-unsupported",fn:()=>u("powershell-unsupported","PowerShell commands are blocked because Tripwire cannot parse PowerShell grammar. Use the Bash tool, or add a PowerShell parser before you enable this tool.")}),s;if(e==="Bash"&&St(t))return tn(t.command,n,r===void 0?{}:{cwd:r});if(e==="Read"&&wt(t)){let a=t;return s.push({name:"read-protect",fn:()=>Ut(a)}),s}let i=(e==="Edit"||e==="MultiEdit")&&bt(t),o=e==="Write"&&kt(t);if(i){let a=t;s.push({name:"path-protect",fn:()=>Te(a)},{name:"lazy-code",fn:()=>He(a)})}else if(o){let a=t;s.push({name:"path-protect",fn:()=>Te(a)},{name:"lazy-code",fn:()=>He(a)})}return s},rn=(e,t,n)=>{if(e==="Bash"||e==="PowerShell"||e==="Read"||e==="WebFetch"){let r=e==="PowerShell"?"Bash":e;return[{name:"post-secret-scrub",fn:()=>zt({toolName:r,response:t,secretScanner:n})}]}return[]},qt=(e,t)=>w.gen(function*(){if(e.length===0)return c("no-rules");let r=[];for(let s of e)r.push(yield*Ps(s.name,s.fn,t));return L(r)}),Oe=(e)=>{if(e.length===0)return c("no-rules");let t=[];for(let n of e)try{t.push(n.fn())}catch(r){A(n.name,r),t.push(c(n.name))}return L(t)},As=(e,t={})=>{let n=J(t),r=oe(e.tool_name??"");if(e.hook_event_name==="PreToolUse")return Oe(nn(r,e.tool_input,n,e.cwd));if(e.hook_event_name==="PostToolUse")return Oe(rn(r,e.tool_response,n.secretScanner));return c("no-rules")},_s=(e,t={},n={})=>Oe(tn(e,J(t),n)),Is=(e,t,n)=>{let r=e.hook_event_name,s=oe(e.tool_name??"");if(r==="PreToolUse"&&s==="Bash"){if(t.kind==="warn"){jt(e,t,n);return}G(n);return}if(t.kind==="warn"){jt(e,t,n);return}G(n)},Ws=(e)=>`tripwire config at ${te} failed to load, so ALL custom safety policy is inactive. Failing closed until it is fixed. Fix the JSON, then this clears on the next hook call. Restart any long-running consumer that caches the loaded config. Error: ${e}`,sn=()=>{let e=process.argv.indexOf("--cursor-event");if(e!==-1)return process.argv[e+1]??"";return process.argv.find((n)=>n.startsWith("--cursor-event="))?.slice(15)},Ts=()=>process.argv.includes(Es),on=()=>{let e=sn();return e===void 0?Be:Pe(e)},D=(e,t=!1)=>{let n=on();if(t){$e("PreToolUse",u("tripwire-batch-error",`Tripwire batch input could not be processed (${e}). Failing closed.`),n);return}if(n.kind==="cursor"&&!n.post){Qt(u("cursor-hook-error",`Cursor hook input could not be processed (${e}). Failing closed.`));return}G(n)},Jt=(e,t)=>{if(t.kind==="cursor")return`cursor:${t.eventName}`;return Xt(e)?"codex":"native"},Zt=(e)=>e.length===1?e[0]??c("no-rules"):L(e),Ds=(e)=>w.gen(function*(){let n=Array.isArray(e);if(n&&!Ts())return A("decode","Batch hook input is only available through --tripwire-hook"),D("unsupported batch input"),{ok:!1};let r,s;if(n){let l=yield*w.exit(ie.decodeEffect(Rs)(e));if(se.isFailure(l))return A("decode",K.pretty(l.cause)),D("unsupported batch event shape",!0),{ok:!1};r=l.value,s=on()}else{let l=yt(e,sn()),f=yield*w.exit(ie.decodeUnknownEffect(_e)(l.event));if(se.isFailure(f))return A("decode",K.pretty(f.cause)),D("unsupported event shape"),{ok:!1};r=[f.value],{host:s}=l}let[i]=r;if(i===void 0)return D("empty batch",!0),{ok:!1};let o=i.hook_event_name,a=Jt(i,s);if(r.some((l)=>l.hook_event_name!==o))return A("decode","Batch hook input mixes event phases"),D("mixed event phases",!0),{ok:!1};if(r.some((l)=>Jt(l,s)!==a))return A("decode","Batch hook input mixes hosts"),D("mixed hosts",!0),{ok:!1};return{ok:!0,events:r,firstEvent:i,host:s,phase:o}}),Hs=w.gen(function*(){let t=yield*ne(),n=yield*w.promise(xs),r=yield*w.exit(w.try({try:()=>JSON.parse(n),catch:(f)=>new en({cause:f})}));if(se.isFailure(r)){A("parse",K.pretty(r.cause)),D("invalid JSON");return}let s=yield*Ds(r.value);if(!s.ok)return;let{events:i,firstEvent:o,host:a,phase:l}=s;if(l==="PreToolUse"){if(!t.ok){$e(l,u("config-error",Ws(t.error)),a);return}let f=[];for(let C of i){let v=oe(C.tool_name??"");f.push(yield*qt(nn(v,C.tool_input,t.config,C.cwd),Gt))}let p=Zt(f);if(p.kind==="deny"||p.kind==="ask"){$e(l,p,a);return}Is(o,p,a);return}if(l==="PostToolUse"){let f=t.ok?t.config.secretScanner:q().secretScanner,p=[];for(let v of i){let H=oe(v.tool_name??"");p.push(yield*qt(rn(H,v.tool_response,f),Gt))}let C=Zt(p);if(C.kind==="deny"){Cs(C,a);return}G(a);return}G(a)}),$o=Hs.pipe(w.catchCause((e)=>w.sync(()=>{A("dispatch-fatal",K.pretty(e)),D("dispatcher failure")})));export{c as allow,m as ask,As as decide,_s as decideBash,u as deny,q as getDefaultConfig,Bn as loadConfig,ne as loadConfigResult,J as mergeWithDefaults,E as warn};
|
package/dist/tripwire-cli.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var a=(e)=>{let n=e?.trim();return n===void 0||n.length===0?void 0:n},
|
|
3
|
+
var a=(e)=>{let n=e?.trim();return n===void 0||n.length===0?void 0:n},s=()=>({bunExecutable:a(process.env.TRIPWIRE_BUN),forcePortable:process.env.TRIPWIRE_FORCE_PORTABLE==="1",packageDist:a(process.env.TRIPWIRE_PACKAGE_DIST)}),c=(e)=>({...process.env,TRIPWIRE_PACKAGE_DIST:e});import{accessSync as p,constants as f,realpathSync as u}from"fs";import{createRequire as g}from"module";import m from"path";import{fileURLToPath as b}from"url";var v=["@seanmozeik","tripwire-darwin-arm64"].join("/"),R=(e)=>{try{return p(e,f.X_OK),u(e)}catch{return}},E=(e,n)=>{try{let r=n?.()??g(e).resolve(v);return R(r)}catch{return}},h=(e)=>{let n=b(e),r=n;try{r=u(n)}catch{}return m.join(m.dirname(r),"tripwire.js")},d=(e={})=>{let n=e.moduleUrl??import.meta.url,r=e.platform??process.platform,i=e.architecture??process.arch,t=s();if(!t.forcePortable&&r==="darwin"&&i==="arm64"){let o=E(n,e.nativeResolver);if(o!==void 0)return{arguments:[],executable:o,kind:"native"}}return{arguments:[e.portableExecutable??h(n)],executable:e.bunExecutable??t.bunExecutable??"bun",kind:"portable"}};var l=(e)=>{let n=d({moduleUrl:import.meta.url}),r=e==="hook"?"--tripwire-hook":"--tripwire-force-cli",i=import.meta.dirname,t=Bun.spawnSync([n.executable,...n.arguments,r,...process.argv.slice(2)],{stderr:"inherit",env:c(i),stdin:"inherit",stdout:"inherit"});process.exit(t.exitCode)};l("cli");
|