@odingard/guardian-hook 0.3.0 → 0.4.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 +26 -4
- package/dist/agent-sdk.js +1142 -13
- package/dist/index.js +1142 -13
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -22,19 +22,30 @@ There is no model in the decision: Guardian decides ALLOW / ASK / DENY determini
|
|
|
22
22
|
|
|
23
23
|
Shell commands are routed by consequence. That routing is what lets you put deploys behind your phone while ordinary commands flow freely.
|
|
24
24
|
|
|
25
|
-
**
|
|
25
|
+
**How a command is routed.** The hook parses the command line and routes it by the riskiest command inside it. It removes quotes and escapes, splits on `;`, `&&`, `||`, `|`, newlines and subshells, and looks inside `$( )`, backticks, `<( )`, `sh -c` / `bash -c` strings, `eval`, `su -c`, `env -S` and `find -exec`. It looks past wrappers (`sudo`, `doas`, `env`, `xargs`, `nice`, `nohup`, `time`, `timeout`, `stdbuf`, `exec`, `command`, `watch`, `npx`), git global options (`-C`, `-c`, `--git-dir`, `--work-tree`) and git aliases set with `-c alias.*`. So `find . | xargs rm -rf`, `git -C repo push -f`, `"git" push --force` and `r\m -rf ~` are all `shell_destructive`. When the hook cannot follow a command, it sends it to the Ask tier: an unterminated quote, a command name known only at run time (`$CMD -rf x`), `eval` or `sh -c` of run-time text (`sh -c "$(echo … | base64 -d)"`, `eval "$(tool init)"`), or a `case` statement.
|
|
26
|
+
|
|
27
|
+
**It is still not a security boundary.** The hook cannot see inside scripts (`bash deploy.sh`, `make clean`), code run by other interpreters (`python -c`, `node -e`, PowerShell), shell functions and aliases, or git aliases and hooks already in the git config. Under prompt injection, treat the Ask tier as advisory. Where that matters, set the everyday connection to **Ask me first** as well, or deny `shell` outright.
|
|
26
28
|
|
|
27
29
|
| Guardian tool | Suggested mode | Covers |
|
|
28
30
|
| --------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
31
|
| `shell` | Allow | Ordinary commands (build, test, inspect) |
|
|
30
32
|
| `shell_deploy` | Ask me first | terraform/tofu apply and destroy, pulumi up, kubectl apply and delete, helm install and upgrade, cloud deploy CLIs, package publish, `gh pr merge` |
|
|
31
|
-
| `shell_destructive` | Ask me first |
|
|
33
|
+
| `shell_destructive` | Ask me first | See the list below |
|
|
32
34
|
| `git_push` | Ask me first | Pushing commits |
|
|
33
35
|
| `shell_session` | Ask me first | Starting an interactive shell, interpreter, database client or remote session (`bash`, `python3`, `ssh host`, `docker exec -it`, `psql app`) |
|
|
34
36
|
| `file_write` | Allow | Write and edit (the approver sees the path, size and a SHA-256 of the content; the content itself is not sent) |
|
|
35
37
|
| `web_fetch` | Allow | Fetching a URL |
|
|
36
38
|
| `mcp.<server>.<tool>` | your choice | Any MCP tool, e.g. `mcp.github.create_pull_request` |
|
|
37
39
|
|
|
40
|
+
`shell_destructive` covers:
|
|
41
|
+
|
|
42
|
+
- `rm -r` / `rm -rf` (also with no operands, and through `xargs`), and any `rm` run by `find -exec`, `find -ok` or `xargs`; `find -delete`; `shred`, `mkfs`, `dd if=`, `wipefs`.
|
|
43
|
+
- `git reset --hard` or `--merge`, `git clean -f`, `git branch -D` (or `-d -f`), `git checkout -f` or `checkout -- <path>`, `git switch --discard-changes`, `git restore --source`, `git stash drop` / `clear`, `git update-ref -d`, `git reflog expire`, `git filter-branch`.
|
|
44
|
+
- Force pushes and remote ref deletes: `git push --force`, `-f`, `--force-with-lease`, `--force-if-includes`, `+refspec`, `--mirror`, `--prune`, `--delete`, `-d`, `origin :branch`, with or without git global options (`git -C dir push -f`).
|
|
45
|
+
- `DROP` / `TRUNCATE` / unqualified `DELETE FROM`, `chmod -R` to a world-writable mode, and downloaded code run by a shell or interpreter (`curl … | sh`, `bash <(curl …)`, `sh -c "$(curl …)"`).
|
|
46
|
+
- Changes to the agent's own MCP servers, hooks or permissions: `claude mcp add`, `add-json`, `add-from-claude-desktop`, `remove` and the other non-read `mcp`, `plugin` and `config` subcommands of `claude`, `codex` and `gemini`; a nested `claude` or `codex` given `--mcp-config`, `--settings`, `--dangerously-skip-permissions` or a wider `--permission-mode`; and writes to `.mcp.json`, `mcp.json`, `~/.claude.json`, `settings.local.json`, `.claude/settings*.json`, `.claude/hooks`, `agents`, `commands`, `skills` or `plugins`, `.codex/`, `.cursor/`, `.gemini/`, `.dsh/` or `guardian-hooks.json`. That includes writes by redirection (`> .mcp.json`), by commands such as `cp`, `mv`, `tee`, `sed -i` and `ln`, and by the file tools (`Write`, `Edit`, `MultiEdit`, `write`, `edit`, `str_replace_editor`, `apply_patch`), which then carry `touches: "agent-config"`. Reading those files stays `shell`.
|
|
47
|
+
- Commands the hook cannot parse (see above).
|
|
48
|
+
|
|
38
49
|
Read-only tools (Read, Grep, Glob, LS, WebSearch) are not routed through Guardian.
|
|
39
50
|
|
|
40
51
|
## Setup
|
|
@@ -241,11 +252,22 @@ A deny is the Claude Code deny JSON, plus the reason on stderr and exit 2, both
|
|
|
241
252
|
- **The bridge lets the call run if the hook fails to run or crashes.** Only a deny or exit 2 blocks. As with Codex, the hook catches its own crashes, exits 2 on deny, and denies at its own 540-second deadline; the `|| … exit 2` covers a hook that cannot start. A hook killed from outside still fails open.
|
|
242
253
|
- Not yet confirmed live: the tool names the bridge passes in a hosted dsh image (the source passes dsh's names unchanged; the matcher lists Claude Code's too), whether a host's dsh image already includes the bridge plugin, whether the hook's environment keeps `GUARDIAN_AGENT_CREDENTIAL`, and whether a host's headless runs allow a 5-minute wait. See `examples/deepseek-harness/`.
|
|
243
254
|
|
|
255
|
+
## Edit-type tools: the target file is pinned (0.4)
|
|
256
|
+
|
|
257
|
+
Approval binds the exact arguments. A file path is an argument; for most tools the file's contents are not. For Edit, MultiEdit, NotebookEdit and DeepSeek Harness `edit`, whose result depends on the file being edited, the hook reads the target file when the call is proposed and puts its SHA-256 in the approved arguments (`guardianContentPins`, or `absent: true` for a file that does not exist yet). Just before it releases the approved call it checks the file again. If the file changed, appeared or disappeared, the call is denied with `CONTENT_CHANGED_SINCE_APPROVAL`, nothing runs and the capability is not redeemed. Guardian's servers never see the file.
|
|
258
|
+
|
|
259
|
+
- What this checks is that the file matched when the approved call was released. The agent's own Edit tool then reads the file again after the hook returns, so a short window remains.
|
|
260
|
+
- The target path must be absolute; the hook does not resolve it against the working directory. An Edit-type call with a relative path is denied (`CONTENT_PIN_INVALID_ARGUMENT`). Claude Code always sends absolute paths.
|
|
261
|
+
- Write and DeepSeek `write` are not pinned: they replace the whole file, so its current contents do not change what runs. Their arguments and tiers are unchanged.
|
|
262
|
+
- Shell commands are not pinned. The command text is bound; files it names (`bash deploy.sh`, `curl -F file=@x`, globs, `$VAR`) are not.
|
|
263
|
+
- Pinning is supported on Linux and macOS. On Windows an Edit-type call is denied (`CONTENT_PIN_UNSUPPORTED_PLATFORM`).
|
|
264
|
+
- **Digest privacy:** the SHA-256 of the target is sent to Guardian, shown to the approver and kept in receipts. For a small or low-entropy file, anyone who sees the digest can guess the contents offline by hashing candidates.
|
|
265
|
+
|
|
244
266
|
## Limits (read these)
|
|
245
267
|
|
|
246
268
|
- **Decision-gated.** The agent performs the effect after Guardian authorizes and redeems the call. The receipt proves Guardian's authorization of that exact call, not the command's own outcome.
|
|
247
|
-
- **Routing is local and evadable.** The
|
|
248
|
-
- **The agent can edit the hook's own config
|
|
269
|
+
- **Routing is local and evadable.** The parser and patterns run on your machine. They do not see inside scripts, other interpreters' code, shell functions or configured git aliases (see [Tools the hook proposes](#tools-the-hook-proposes)).
|
|
270
|
+
- **The agent can still edit the hook's own config in ways the hook does not recognise.** Writes to the known config files (`.codex/hooks.json`, `guardian-hooks.json`, `cordis.patch.yml`, `.claude/settings.json` and the others listed above) go to the Ask tier, but a script, an interpreter (`python script.py`) or a path built at run time can still rewrite them and remove the hook. On one-shot dsh runs the harness restarts on every message, so the edit takes effect on the next one. Deliver the hook as a managed hook (Codex `requirements.toml` with `allow_managed_hooks_only`, Claude Code managed settings), or mount the config read-only (owned by another user) where that matters.
|
|
249
271
|
- **Starting the hook.** It answers only when run as `guardian-hook` (or `…/hook/dist/index.js`). Installed under another file name it exits 0 with no output, which every agent treats as "no objection". Use the documented commands.
|
|
250
272
|
- **Only calls through the hook are governed.** Commands you run yourself, or other credentials on the machine, are not.
|
|
251
273
|
- **Arguments are bounded.** Commands over 2,000 characters are truncated in what the approver sees; MCP inputs over that size are sent as a digest.
|