@loophubs/agent-guard 0.1.0 → 0.2.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 CHANGED
@@ -1,91 +1,15 @@
1
- # agent-guard
1
+ # Agent Guard
2
2
 
3
- `agent-guard` is a macOS pre-tool hook for Claude Code, Codex, and Pi that blocks risky filesystem scans and credential reads. It helps avoid agent commands that traverse `~/Library` and trigger macOS App Data prompts recorded as `SystemPolicyAppDataDetailed`.
3
+ `agent-guard` is a macOS pre-tool guard for Claude Code, Codex, and Pi. It checks supported tool calls before execution and blocks risky filesystem scans and credential reads.
4
4
 
5
- ## Install
5
+ macOS can record App Data access as `SystemPolicyAppDataDetailed` when a command reads or enumerates another app's data under `~/Library`. Broad scans from the home directory can cross those containers and trigger repeated permission prompts.
6
6
 
7
- Install globally with npm or Bun (Bun 1.4 or newer must be available at runtime):
7
+ ## What it does
8
8
 
9
- ```sh
10
- npm install --global @loophubs/agent-guard
11
- # or
12
- bun add --global @loophubs/agent-guard
13
- command -v agent-guard
14
- ```
9
+ The guard blocks broad home and `~/Library` walks, protected App Data access, and recognizable attempts to display credentials. It allows project-scoped searches and named non-sensitive file reads; a clean guard result still leaves the runtime's own permissions in force. It covers only calls routed through a registered hook, not every tool or process.
15
10
 
16
- Use the absolute path printed by the last command in the examples below.
11
+ To install the package and register and verify a runtime, follow [the setup guide](docs/setup.md).
17
12
 
18
- ## Register a runtime
13
+ [GitHub Releases](https://github.com/LoopHubs/agent-guard/releases) describe version-specific changes to what the guard blocks or allows.
19
14
 
20
- ### Claude Code
21
-
22
- Add this `PreToolUse` hook to `~/.claude/settings.json`, replacing the command path. It covers Claude Code's Bash and file-reading tools; [Claude Code's hook settings](https://code.claude.com/docs/en/hooks#hook-locations) describe the user-level location.
23
-
24
- ```json
25
- {
26
- "hooks": {
27
- "PreToolUse": [{
28
- "matcher": "Bash|Read|Edit|Write|Grep",
29
- "hooks": [{ "type": "command", "command": "/absolute/path/to/agent-guard --runtime claude", "timeout": 5 }]
30
- }]
31
- }
32
- }
33
- ```
34
-
35
- ### Codex
36
-
37
- Add this to `~/.codex/config.toml`, replacing the command path. [OpenAI's hook documentation](https://learn.chatgpt.com/docs/hooks) specifies `PreToolUse`, the Bash matcher, and `/hooks` trust review; open `/hooks` in Codex and trust the new hook before testing it.
38
-
39
- ```toml
40
- [[hooks.PreToolUse]]
41
- matcher = "^Bash$"
42
-
43
- [[hooks.PreToolUse.hooks]]
44
- type = "command"
45
- command = "/absolute/path/to/agent-guard --runtime codex"
46
- timeout = 5
47
- ```
48
-
49
- ### Pi
50
-
51
- Create `~/.pi/agent/extensions/agent-guard.ts` with this adapter, replacing the command path. [Pi's extension API](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md) loads files in that directory and lets `tool_call` block a tool before execution.
52
-
53
- ```ts
54
- import { spawnSync } from "node:child_process";
55
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
56
-
57
- const guard = "/absolute/path/to/agent-guard";
58
-
59
- export default function (pi: ExtensionAPI) {
60
- pi.on("tool_call", (event, ctx) => {
61
- if (!["bash", "read", "edit", "write", "grep"].includes(event.toolName)) return;
62
- const input = event.input as Record<string, unknown>;
63
- const toolInput = event.toolName === "bash" ? input : {
64
- ...input,
65
- file_path: input.path,
66
- };
67
- const result = spawnSync(guard, ["--runtime", "pi"], {
68
- cwd: ctx.cwd,
69
- input: JSON.stringify({ tool_name: event.toolName, tool_input: toolInput, cwd: ctx.cwd }),
70
- encoding: "utf8",
71
- timeout: 4500,
72
- });
73
- if (result.status === 0) return;
74
- return { block: true, reason: result.stderr?.trim() || result.error?.message || "agent-guard failed" };
75
- });
76
- }
77
- ```
78
-
79
- ## Verify it blocks
80
-
81
- From a project directory, ask the configured agent to run `du -sh ~/Library`. It should report the guard's denial **without running `du`**. You can check the package entry directly without reading `~/Library`:
82
-
83
- ```sh
84
- printf '%s\n' '{"tool_name":"Bash","tool_input":{"command":"du -sh ~/Library"}}' | agent-guard --runtime claude
85
- ```
86
-
87
- The direct check should exit 2 and print a `DENIED:` reason on stderr. A direct check confirms the package works; the agent check confirms that runtime loaded its hook.
88
-
89
- ## Limits
90
-
91
- The guard checks supported tool calls and recognizable shell commands; it does not sandbox the agent or cover every custom tool, dynamic shell expansion, or process outside a registered hook. Codex's example covers Bash calls, while the Pi adapter covers Bash, read, edit, write, and grep; other Pi tools need their own adapter handling. A broken or timed-out package entry denies a checked call, but a hook disabled or skipped by its runtime cannot inspect that call.
15
+ > **AI agents:** Read `docs/setup.md` first, then ask your user whether to install and register the guard. Do not install it or change configuration without consent.
package/bin/agent-guard CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/zsh -f
2
2
  # PreToolUse entry for the agent guard:
3
3
  # agent-guard --runtime claude|codex|pi < event.json
4
- # Runs src/guard.ts from the package directory and passes its denial through
4
+ # Runs the Bun supervisor from the package directory and passes denials through
5
5
  # with exit 2. Exit 0 only means no objection; it never approves a call. Every
6
6
  # other outcome denies too, because Claude lets a hook's other failures and
7
7
  # timeouts through, Codex is assumed to, and nothing else stands between an
@@ -15,24 +15,28 @@ fail() {
15
15
  exit 2
16
16
  }
17
17
 
18
+ [[ $HOME == /* ]] || fail 'HOME is not an absolute path'
18
19
  pkg=${0:A:h:h}
19
20
  bun=$(command -v bun) || fail 'bun missing'
20
21
  guard=$pkg/src/guard.ts
21
22
  [[ -r $guard ]] || fail 'guard.ts missing'
23
+ runner=$pkg/src/runner.ts
24
+ [[ -r $runner ]] || fail 'runner.ts missing'
25
+ [[ -r $pkg/tsconfig.json ]] || fail 'tsconfig.json missing'
22
26
  event=$(/bin/cat)
23
- # bun reads bunfig.toml, tsconfig.json and .env from its working directory,
24
- # which is the agent's project, so a project file could preload code into the
25
- # guard or remap its parser. The guard runs from the package directory, with
26
- # the hook's directory passed as --cwd.
27
+ # Bun reads bunfig.toml and environment files from its working directory;
28
+ # the package config owns TypeScript paths. Pass the hook's directory as --cwd.
27
29
  hook_cwd=$PWD
28
30
  cd -q $pkg || fail 'package directory unreadable'
29
31
 
30
- # The deadline sits below every runtime's hook timeout, so an overrun denies
31
- # here instead of timing out as a pass. Reading the coprocess under the same
32
- # deadline keeps anything the guard started from holding this hook open.
33
- coproc { print -r -- "$event" | exec $bun $guard "$@" --cwd $hook_cwd 2>&1 }
32
+ # The runner kills the guard's process group. This outer deadline still denies
33
+ # if the runner itself does not return before the hook timeout.
34
+ coproc { print -r -- "$event" | exec $bun $runner "$@" --cwd $hook_cwd 2>&1 }
34
35
  pid=$!
35
- { zselect -t 300; kill -KILL $pid } >/dev/null 2>&1 &!
36
+ read -r -t 1 -p group || { kill -KILL $pid 2>/dev/null; fail 'supervisor did not report its process group' }
37
+ [[ $group == AGENT_GUARD_PGID=<-> ]] || { kill -KILL $pid 2>/dev/null; fail 'invalid supervisor process group' }
38
+ pgid=${group#AGENT_GUARD_PGID=}
39
+ { zselect -t 300; kill -KILL -- -$pgid; kill -KILL $pid } >/dev/null 2>&1 &!
36
40
  watchdog=$!
37
41
  read -r -t 3 -p reason
38
42
  wait $pid
package/package.json CHANGED
@@ -1,26 +1,47 @@
1
1
  {
2
2
  "name": "@loophubs/agent-guard",
3
- "version": "0.1.0",
4
- "type": "module",
3
+ "version": "0.2.0",
5
4
  "description": "Blocks filesystem scans and credential reads that macOS records as TCC App Data access, and points the agent at a safe alternative.",
5
+ "homepage": "https://github.com/LoopHubs/agent-guard#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/LoopHubs/agent-guard/issues"
8
+ },
6
9
  "license": "MIT",
7
- "engines": {
8
- "bun": ">=1.4"
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/LoopHubs/agent-guard.git"
9
13
  },
10
14
  "bin": {
11
15
  "agent-guard": "bin/agent-guard"
12
16
  },
13
17
  "files": [
14
18
  "bin",
15
- "src"
19
+ "src",
20
+ "tsconfig.json"
16
21
  ],
22
+ "type": "module",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "scripts": {
27
+ "test": "bun test tests/",
28
+ "typecheck": "tsc --noEmit",
29
+ "lint": "oxlint . && oxfmt --check .",
30
+ "format": "oxfmt .",
31
+ "check": "bun run lint && bun run typecheck && bun run test"
32
+ },
17
33
  "dependencies": {
18
34
  "mvdan-sh": "0.10.1"
19
35
  },
20
36
  "devDependencies": {
21
- "typescript": "6.0.3"
37
+ "@types/bun": "1.4.2",
38
+ "@types/mvdan-sh": "0.10.9",
39
+ "@types/node": "26.6.3",
40
+ "oxfmt": "0.71.0",
41
+ "oxlint": "1.86.0",
42
+ "typescript": "7.0.2"
22
43
  },
23
- "scripts": {
24
- "test": "bunx tsc --noEmit --strict --target es2022 --module esnext --moduleResolution bundler --allowImportingTsExtensions tests/*.ts tests/fixtures/*.ts && bun test tests/"
44
+ "engines": {
45
+ "bun": ">=1.4"
25
46
  }
26
47
  }
package/src/argv.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { basename, resolve } from "node:path";
2
- import { expandHome } from "./paths.ts";
3
- import type { Command, Word } from "./record.ts";
4
- import { searchRoles } from "./search-roles.ts";
2
+
3
+ import { expandHome } from "./paths";
4
+ import type { Command, Word } from "./record";
5
+ import { searchRoles } from "./search-roles";
5
6
 
6
7
  const shellPrograms = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
7
8
  const codePrograms = new Set(["python", "python3", "node", "bun", "deno", "ruby", "perl", "php", "osascript", "lua"]);
@@ -17,82 +18,82 @@ export function resolveCommand(cmd: Command, home: string): { children: string[]
17
18
  let lastPrecommand = "";
18
19
  // Bash reads an assignment after nocorrect as an argument; zsh does not.
19
20
  const assignment = (word: Word) => word.role === "assign" || /^[A-Za-z_][A-Za-z0-9_]*=/.test(word.raw);
20
- while (i < w.length && (assignment(w[i]) || w[i].raw === "nocorrect")) w[i++].role = "precommand";
21
- while (i < w.length && ["builtin", "-", "noglob"].includes(w[i].text)) {
22
- lastPrecommand = w[i].text;
23
- w[i++].role = "precommand";
21
+ while (i < w.length && (assignment(w[i]!) || w[i]!.raw === "nocorrect")) w[i++]!.role = "precommand";
22
+ while (i < w.length && ["builtin", "-", "noglob"].includes(w[i]!.text)) {
23
+ lastPrecommand = w[i]!.text;
24
+ w[i++]!.role = "precommand";
24
25
  }
25
26
  // builtin runs nothing when the next word is not a builtin.
26
27
  if (lastPrecommand === "builtin" && !zshBuiltins.has(w[i]?.text ?? "")) i = w.length;
27
28
  wrappers: while (i < w.length) {
28
- const name = basename(w[i].text);
29
+ const name = basename(w[i]!.text);
29
30
  switch (name) {
30
31
  case "command":
31
- if (w[i].text !== "command") break wrappers;
32
- w[i++].role = "precommand";
33
- while (["-p", "--"].includes(w[i]?.text)) i++;
32
+ if (w[i]!.text !== "command") break wrappers;
33
+ w[i++]!.role = "precommand";
34
+ while (["-p", "--"].includes(w[i]?.text ?? "")) i++;
34
35
  // command -v only looks the name up.
35
- if (["-v", "-V"].includes(w[i]?.text)) i = w.length;
36
+ if (["-v", "-V"].includes(w[i]?.text ?? "")) i = w.length;
36
37
  break;
37
38
  case "exec":
38
- if (!shell || w[i].text !== "exec") break wrappers;
39
- w[i++].role = "precommand";
40
- while (["-c", "-l"].includes(w[i]?.text)) i++;
39
+ if (!shell || w[i]!.text !== "exec") break wrappers;
40
+ w[i++]!.role = "precommand";
41
+ while (["-c", "-l"].includes(w[i]?.text ?? "")) i++;
41
42
  if (w[i]?.text === "-a") i += 2;
42
43
  break;
43
44
  case "nohup":
44
- w[i++].role = "precommand";
45
+ w[i++]!.role = "precommand";
45
46
  if (w[i]?.text === "--") i++;
46
47
  break;
47
48
  case "timeout":
48
- w[i++].role = "precommand";
49
- while (w[i]?.text.startsWith("-")) {
50
- if (["-s", "--signal", "-k", "--kill-after"].includes(w[i].text)) i++;
49
+ w[i++]!.role = "precommand";
50
+ while (w[i]?.text?.startsWith("-")) {
51
+ if (["-s", "--signal", "-k", "--kill-after"].includes(w[i]!.text)) i++;
51
52
  i++;
52
53
  }
53
54
  i++; // duration
54
55
  break;
55
56
  case "nice":
56
- w[i++].role = "precommand";
57
+ w[i++]!.role = "precommand";
57
58
  if (w[i]?.text === "-n" || w[i]?.text === "--adjustment") i += 2;
58
59
  else if (/^(-n|--adjustment=|-\d)/.test(w[i]?.text ?? "")) i++;
59
60
  break;
60
61
  case "stdbuf":
61
- w[i++].role = "precommand";
62
+ w[i++]!.role = "precommand";
62
63
  while (/^-[ioe]/.test(w[i]?.text ?? "")) {
63
- if (/^-[ioe]$/.test(w[i].text)) i++;
64
+ if (/^-[ioe]$/.test(w[i]!.text)) i++;
64
65
  i++;
65
66
  }
66
67
  break;
67
68
  case "caffeinate":
68
- w[i++].role = "precommand";
69
+ w[i++]!.role = "precommand";
69
70
  while (/^-[disumtw]$/.test(w[i]?.text ?? "")) {
70
- if (w[i].text === "-t" || w[i].text === "-w") i++;
71
+ if (w[i]!.text === "-t" || w[i]!.text === "-w") i++;
71
72
  i++;
72
73
  }
73
74
  break;
74
75
  case "time":
75
- w[i++].role = "precommand";
76
- while (w[i]?.text.startsWith("-")) {
77
- if (["-f", "-o", "--format", "--output"].includes(w[i].text)) i++;
76
+ w[i++]!.role = "precommand";
77
+ while (w[i]?.text?.startsWith("-")) {
78
+ if (["-f", "-o", "--format", "--output"].includes(w[i]!.text)) i++;
78
79
  i++;
79
80
  }
80
81
  break;
81
82
  case "xargs":
82
- w[i++].role = "precommand";
83
- while (w[i]?.text.startsWith("-")) {
84
- if (["-a", "-d", "-E", "-I", "-L", "-n", "-P", "-s", "--arg-file", "--delimiter", "--replace", "--max-args"].includes(w[i].text)) i++;
83
+ w[i++]!.role = "precommand";
84
+ while (w[i]?.text?.startsWith("-")) {
85
+ if (["-a", "-d", "-E", "-I", "-L", "-n", "-P", "-s", "--arg-file", "--delimiter", "--replace", "--max-args"].includes(w[i]!.text)) i++;
85
86
  i++;
86
87
  }
87
88
  break;
88
89
  case "env":
89
- w[i++].role = "precommand";
90
+ w[i++]!.role = "precommand";
90
91
  while (i < w.length) {
91
- const arg = w[i].text;
92
+ const arg = w[i]!.text;
92
93
  if (arg === "-C") {
93
94
  if (w[i + 1]) {
94
- cmd.cwd = resolve(cmd.cwd, w[i + 1].text);
95
- w[i + 1].role = "precommand";
95
+ cmd.cwd = resolve(cmd.cwd, w[i + 1]!.text);
96
+ w[i + 1]!.role = "precommand";
96
97
  }
97
98
  i += 2;
98
99
  } else if (["-u", "-P"].includes(arg)) i += 2;
@@ -107,15 +108,15 @@ export function resolveCommand(cmd: Command, home: string): { children: string[]
107
108
  break;
108
109
  case "repeat":
109
110
  // zsh's repeat COUNT runs the rest; bash parses it as a plain command.
110
- if (!shell || w[i].text !== "repeat") break wrappers;
111
- w[i++].role = "precommand";
111
+ if (!shell || w[i]!.text !== "repeat") break wrappers;
112
+ w[i++]!.role = "precommand";
112
113
  i++;
113
114
  continue;
114
115
  case "envchain":
115
- w[i++].role = "precommand";
116
+ w[i++]!.role = "precommand";
116
117
  // --set, --list and --unset manage the store and run nothing.
117
- if (w[i]?.text.startsWith("-")) i = w.length;
118
- if (w[i]) w[i].role = "namespace";
118
+ if (w[i]?.text?.startsWith("-")) i = w.length;
119
+ if (w[i]) w[i]!.role = "namespace";
119
120
  i++;
120
121
  break;
121
122
  default:
@@ -127,25 +128,30 @@ export function resolveCommand(cmd: Command, home: string): { children: string[]
127
128
  cmd.shell = shell;
128
129
  if (i >= w.length) return { children, code };
129
130
  cmd.program = i;
130
- w[i].role = "program";
131
- if (w[i].text.startsWith("=") && w[i].text.length > 1) w[i].text = w[i].text.slice(1);
132
- const name = basename(w[i].text);
131
+ w[i]!.role = "program";
132
+ if (w[i]!.text.startsWith("=") && w[i]!.text.length > 1) w[i]!.text = w[i]!.text.slice(1);
133
+ const name = basename(w[i]!.text);
133
134
  const rest = w.slice(i + 1);
134
135
  if (["rg", "grep", "ag", "ack"].includes(name)) {
135
136
  searchRoles(cmd, rest, name);
136
137
  } else if (name === "fd") {
137
138
  let pattern = true;
138
139
  for (let n = 0; n < rest.length; n++) {
139
- const arg = rest[n].text;
140
+ const arg = rest[n]!.text;
140
141
  if (["-x", "-X", "--exec", "--exec-batch"].includes(arg)) {
141
- rest[n].role = "option";
142
- children.push(rest.slice(n + 1).map((word) => word.raw).join(" "));
142
+ rest[n]!.role = "option";
143
+ children.push(
144
+ rest
145
+ .slice(n + 1)
146
+ .map((word) => word.raw)
147
+ .join(" "),
148
+ );
143
149
  break;
144
150
  } else if ((/^(--search-path|--base-directory)(=|$)/.test(arg) || arg === "-C") && (arg.includes("=") || rest[n + 1])) {
145
151
  const separate = !arg.includes("=");
146
152
  const base = arg === "-C" || arg.startsWith("--base-directory");
147
- if (separate) rest[n++].role = "option";
148
- const path = rest[n];
153
+ if (separate) rest[n++]!.role = "option";
154
+ const path = rest[n]!;
149
155
  path.role = "path";
150
156
  path.value = separate ? path.text : arg.slice(arg.indexOf("=") + 1);
151
157
  if (base) {
@@ -153,24 +159,26 @@ export function resolveCommand(cmd: Command, home: string): { children: string[]
153
159
  cmd.cwd = resolve(cmd.cwd, path.value);
154
160
  }
155
161
  } else if (["-E", "-e", "-t", "-d", "--exclude", "--extension", "--type", "--max-depth"].includes(arg)) {
156
- rest[n].role = "option";
157
- if (rest[n + 1]) rest[++n].role = "optarg";
158
- } else if (arg.startsWith("-")) rest[n].role = "option";
159
- else if (pattern) (rest[n].role = "pattern"), (pattern = false);
160
- else rest[n].role = "path";
162
+ rest[n]!.role = "option";
163
+ if (rest[n + 1]) rest[++n]!.role = "optarg";
164
+ } else if (arg.startsWith("-")) rest[n]!.role = "option";
165
+ else if (pattern) {
166
+ rest[n]!.role = "pattern";
167
+ pattern = false;
168
+ } else rest[n]!.role = "path";
161
169
  }
162
170
  } else if (name === "du") {
163
171
  for (let n = 0; n < rest.length; n++) {
164
- const arg = rest[n].text;
172
+ const arg = rest[n]!.text;
165
173
  if (["-d", "-I", "-B", "-t", "--max-depth", "--exclude", "--block-size", "--threshold"].includes(arg) || /^-[^-]+[dIBt]$/.test(arg)) {
166
- rest[n].role = "option";
167
- if (rest[n + 1]) rest[++n].role = "optarg";
168
- } else if (arg.startsWith("-")) rest[n].role = "option";
174
+ rest[n]!.role = "option";
175
+ if (rest[n + 1]) rest[++n]!.role = "optarg";
176
+ } else if (arg.startsWith("-")) rest[n]!.role = "option";
169
177
  }
170
178
  } else if (shellPrograms.has(name)) {
171
179
  const flag = rest.findIndex((a) => shellCodeFlag.test(a.text));
172
180
  if (flag >= 0) {
173
- if (rest[flag + 1]) rest[flag + 1].role = "code";
181
+ if (rest[flag + 1]) rest[flag + 1]!.role = "code";
174
182
  children.push(rest[flag + 1]?.text ?? "");
175
183
  }
176
184
  } else if (name === "eval" && (shell || cmd.wrappers.includes("command"))) {
@@ -178,7 +186,7 @@ export function resolveCommand(cmd: Command, home: string): { children: string[]
178
186
  } else if (codePrograms.has(name)) {
179
187
  const flag = rest.findIndex((a) => /^(-[ceE]|--eval)$/.test(a.text));
180
188
  if (flag >= 0) {
181
- if (rest[flag + 1]) rest[flag + 1].role = "code";
189
+ if (rest[flag + 1]) rest[flag + 1]!.role = "code";
182
190
  code.push(rest[flag + 1]?.text ?? "");
183
191
  }
184
192
  }
package/src/comments.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  // mvdan/sh 3.5 starts a comment at a # right after a quote or an expansion,
2
2
  // where bash and zsh keep the # in the word and run the rest of the line.
3
- import sh from "mvdan-sh";
3
+
4
4
  import type { Comment, File } from "mvdan-sh";
5
+ import sh from "mvdan-sh";
5
6
 
6
7
  export function misreadComment(file: File, bytes: Buffer): boolean {
7
8
  let misread = false;
8
9
  sh.syntax.Walk(file, (node) => {
9
10
  if (node && sh.syntax.NodeType(node) === "Comment") {
10
11
  const at = (node as Comment).Hash.Offset();
11
- misread ||= at > 0 && !" \t\n;&|()<>".includes(String.fromCharCode(bytes[at - 1]));
12
+ misread ||= at > 0 && !" \t\n;&|()<>".includes(String.fromCharCode(bytes[at - 1]!));
12
13
  }
13
14
  return !misread;
14
15
  });
package/src/core.ts CHANGED
@@ -1,16 +1,31 @@
1
- import { parseScript } from "./frontend.ts";
2
- import { linkedRequest } from "./links.ts";
3
- import { absPath } from "./paths.ts";
4
- import { reasons } from "./reasons.ts";
5
- import type { Request, Runtime, Tool } from "./record.ts";
6
- import { appdataRules } from "./rules/appdata.ts";
7
- import { credentialFilesystemRules, credentialRules } from "./rules/credentials.ts";
8
- import { claudeWorkflowRules, codexWorkflowRules } from "./rules/workflow.ts";
1
+ import { parseScript } from "./frontend";
2
+ import { linkedRequest } from "./links";
3
+ import { absPath } from "./paths";
4
+ import { reasons } from "./reasons";
5
+ import type { Request, Runtime, Tool } from "./record";
6
+ import { appdataRules } from "./rules/appdata";
7
+ import { credentialFilesystemRules, credentialRules } from "./rules/credentials";
8
+ import { claudeWorkflowRules } from "./rules/workflow";
9
9
 
10
10
  export function buildRequest(runtime: Runtime, tool: Tool, cwd: string, input: string, glob: string, home: string): Request {
11
- const req: Request = { runtime, tool, home, cwd: absPath(cwd || "/", "/", home), operation: "", target: "", searchRoot: "", glob, commands: [], uninspectable: [] };
11
+ const inputCwd = cwd || "/";
12
+ const req: Request = {
13
+ runtime,
14
+ tool,
15
+ home,
16
+ cwd: absPath(inputCwd, "/", home),
17
+ inputCwd,
18
+ pathInput: input,
19
+ operation: "",
20
+ target: "",
21
+ searchRoot: "",
22
+ glob,
23
+ commands: [],
24
+ uninspectable: [],
25
+ parseFailed: false,
26
+ };
12
27
  if (tool === "bash") {
13
- if (input) Object.assign(req, parseScript(input, req.cwd, home));
28
+ if (input) Object.assign(req, parseScript(input, inputCwd, home));
14
29
  } else if (tool === "grep") {
15
30
  req.operation = "search";
16
31
  req.searchRoot = absPath(input || req.cwd, req.cwd, home);
@@ -22,10 +37,8 @@ export function buildRequest(runtime: Runtime, tool: Tool, cwd: string, input: s
22
37
  }
23
38
 
24
39
  export function evaluate(req: Request): string | undefined {
25
- const denials = [
26
- ...appdataRules(req),
27
- ...credentialRules(req),
28
- ];
40
+ if (req.parseFailed) return reasons.syntax;
41
+ const denials = [...appdataRules(req), ...credentialRules(req)];
29
42
  // Deny lexically protected paths before asking the filesystem about links.
30
43
  if (!denials.length) {
31
44
  try {
@@ -40,5 +53,5 @@ export function evaluate(req: Request): string | undefined {
40
53
  }
41
54
 
42
55
  export function suggestions(req: Request): string[] {
43
- return req.runtime === "claude" ? claudeWorkflowRules(req) : req.runtime === "codex" ? codexWorkflowRules(req) : [];
56
+ return req.runtime === "claude" ? claudeWorkflowRules(req) : [];
44
57
  }