prae-cli 0.9.2 → 0.10.1

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.
@@ -0,0 +1,192 @@
1
+ # Pilot: PRAE on one Claude Code laptop
2
+
3
+ PRAE runs as a Claude Code `PreToolUse` hook. Before every tool call (built-in
4
+ tools such as `Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`, `WebFetch`, and
5
+ every MCP tool `mcp__<server>__<tool>`), Claude Code runs `prae hook
6
+ claude-code`. The hook judges the call against a policy pack with the same
7
+ kernel the other PRAE harnesses use, and appends the verdict to a signed,
8
+ hash-chained ledger on this machine.
9
+
10
+ It starts in **shadow mode**. Shadow mode records every verdict and blocks
11
+ nothing. When the record looks right, one command switches it to **enforce**.
12
+
13
+ There is no cloud account, no open port, no network call and no service to run.
14
+
15
+ **Verified on Claude Code 2.1.233**, from a fresh clone, by following this
16
+ guide in a temporary project:
17
+
18
+ - **Shadow.** One headless session made 10 tool calls: `Read`, `Write`, `Edit`,
19
+ `Bash`, `Glob`, `Grep`, `WebFetch` and `mcp__prae__read_file`. All 10 ran,
20
+ and each one was sealed with its verdict. The `.env` read was sealed as a
21
+ shadow deny (`BASE-005`), and `rm -rf` as a shadow hold (`BASE-002`).
22
+ - **Enforce.** Claude Code refused these: the `.env` read, the same read
23
+ through the MCP tool, a fetch to an unlisted host, edits to the pack and
24
+ to the hook settings, a shell command truncating the pack, and a read of
25
+ the signing key. `rm -rf` and a write outside the workspace were held, and
26
+ `claude -p` refused them. Nothing protected changed on disk.
27
+ - **Record.** `prae verify` passed the chain, with 25 of 25 signatures.
28
+
29
+ ## 1. Install
30
+
31
+ You need Node 22.19 or later. Install the CLI globally, then preview the
32
+ change and apply it:
33
+
34
+ ```bash
35
+ npm install -g prae-cli # prae-cli 0.10.0 or later carries the hook
36
+ prae init claude-code --scope user # prints the settings diff, writes nothing
37
+ prae init claude-code --scope user --apply # installs, shadow mode
38
+ ```
39
+
40
+ Install globally rather than with `npx prae-cli init …`: the settings entry
41
+ runs the `prae` it was installed from, and an `npx` copy lives in npm's
42
+ temporary cache, which npm may clear — the hook would then stop running, and
43
+ Claude Code 2.1.233 lets every call through when a hook cannot start.
44
+
45
+ You must name a target. `--scope user` is `~/.claude/settings.json`, which
46
+ covers every project. `--scope project` is `./.claude/settings.json`.
47
+ `--settings <file>` is any file you name, for example
48
+ `.claude/settings.local.json`. Nothing is written without `--apply`. The file
49
+ is written back as 2-space JSON, the format Claude Code itself writes, so a
50
+ hand-formatted file shows layout changes in the diff. Its values do not change.
51
+
52
+ `init` adds one entry and leaves every other setting and hook as it was:
53
+
54
+ ```json
55
+ "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "timeout": 10,
56
+ "command": "'/opt/homebrew/bin/node' '…/lib/node_modules/prae-cli/lib/bin.js' hook claude-code --shadow --pack '~/.prae/claude-code/prae-baseline.prae.json' --ledger '~/.prae/claude-code/ledger.jsonl' --key '~/.prae/claude-code/prae-signing.pem' --settings '~/.claude/settings.json'" }] }]
57
+ ```
58
+
59
+ The real file holds absolute paths. The command names the node binary and
60
+ PRAE's own script by absolute path, so it does not depend on `PATH`. If you
61
+ upgrade or reinstall prae-cli, or switch Node versions, run `init` again. Claude Code reloads settings on its own,
62
+ so new tool calls go through PRAE without a restart.
63
+
64
+ Options:
65
+
66
+ - `--pack <file>`: use your own pack. The default copies the built-in baseline
67
+ (13 rules: download-and-execute, `rm -rf`, privilege escalation, block devices,
68
+ credential files, writes outside the workspace, force-push, publishes, and
69
+ credential stores — the macOS Keychain through `security` and its files,
70
+ `.netrc`, `.git-credentials`, `.npmrc`, Docker, kube, GnuPG, gh and gcloud
71
+ config — read directly or named in a shell command).
72
+ `prae check <file>` summarises a pack.
73
+ - `--ledger <file>`: write the ledger somewhere else.
74
+ - `--dir <dir>`: keep PRAE's state somewhere other than `~/.prae/claude-code/`.
75
+
76
+ ## 2. What it records, and where
77
+
78
+ Everything lives in `~/.prae/claude-code/`, which is created with mode 0700:
79
+
80
+ | File | What it is |
81
+ | --- | --- |
82
+ | `ledger.jsonl` | One row for each tool call. Every Claude Code session on the machine appends to this one file, under a lock, so parallel tool calls and parallel sessions stay one chain. |
83
+ | `prae-signing.pem` | The Ed25519 key that signs each row. It is generated once with mode 0600. |
84
+ | `prae-signing.pub.pem` | The public half. Give it to whoever verifies the record. |
85
+ | `prae-baseline.prae.json` | The pack in force, unless you passed `--pack`. |
86
+
87
+ Each row holds the time, the Claude Code `session_id`, the `tool_use_id` (as
88
+ `correlation`), the tool, its capability (`fs:read`, `fs:write`,
89
+ `proc:spawn`, `net:fetch`), the subject, the verdict, the rule and its reason,
90
+ the pack's name, version and sha256, and, in shadow mode, `shadow: true`. The
91
+ subject is the path or URL only. A shell call is stored as its program word
92
+ and a hash, never the command line. File contents and tool output are never
93
+ stored.
94
+
95
+ Read the record:
96
+
97
+ ```bash
98
+ P=~/.prae/claude-code
99
+ prae verify $P/ledger.jsonl --key $P/prae-signing.pub.pem
100
+ prae report $P/ledger.jsonl --pack $P/prae-baseline.prae.json --out report.html
101
+ prae console $P/ledger.jsonl --pack $P/prae-baseline.prae.json --out console/
102
+ ```
103
+
104
+ In the report and the console, a shadow verdict is marked `· SHADOW`. It is
105
+ never counted as vetoed or held.
106
+
107
+ ## 3. From shadow to enforce
108
+
109
+ ```bash
110
+ prae init claude-code --scope user --enforce --apply
111
+ ```
112
+
113
+ This replaces PRAE's entry. It does not add a second one. In enforce mode:
114
+
115
+ | Verdict | What Claude Code does |
116
+ | --- | --- |
117
+ | allow | The hook prints nothing. Claude Code's own permission flow decides, as before. PRAE never answers `allow`, because that would skip your permission prompt. |
118
+ | deny | `permissionDecision: "deny"`. The call does not run, and Claude reads the reason, for example `PRAE BASE-005: … (ledger row 12)`. |
119
+ | ask | `permissionDecision: "ask"`. You get a permission prompt. In a headless `claude -p` run nobody can answer it, so Claude Code refuses the call (verified on 2.1.233). |
120
+ | gate error | The pack does not parse, the ledger cannot be written, the key is unreadable, the payload is broken, or the ledger lock is held for more than 3 seconds. The hook refuses the call (exit 2) and records the error in the ledger when it can. |
121
+
122
+ In shadow mode a gate error lets the call run. It shows a `systemMessage`
123
+ warning and records a `PRAE-ERROR` row marked `shadow`.
124
+
125
+ ## 4. Uninstall
126
+
127
+ ```bash
128
+ prae init claude-code --scope user --remove --apply
129
+ ```
130
+
131
+ This removes PRAE's hook entry and nothing else. The ledger and the key stay
132
+ in place, because they are the evidence of what ran. Delete
133
+ `~/.prae/claude-code/` yourself when you no longer need them.
134
+
135
+ ## 5. Privacy
136
+
137
+ Nothing leaves the machine. The hook reads the payload Claude Code passes on
138
+ stdin, reads the pack, appends to a local file, and exits. It opens no port
139
+ and makes no network request. A `WebFetch` is judged by its URL and is never
140
+ fetched by PRAE. For credential checks the gate reads the file the agent is
141
+ about to read, in its own process, and only the finding (type, line,
142
+ confidence) reaches the ledger.
143
+
144
+ ## 6. Limits, stated plainly
145
+
146
+ - **A hook that cannot start does not block, on current Claude Code.** In
147
+ enforce mode, PRAE refuses whenever it runs and fails: a bad pack, an
148
+ unwritable ledger, an exception, its own 3 s lock deadline, or broken
149
+ input. Before Claude Code 2.1.295, though, a hook that cannot start at all
150
+ (node or the checkout moved) or that hits Claude Code's 10 s timeout lets
151
+ the call through. This was checked on 2.1.233. Enforce writes
152
+ `onFailure: "block"`, which Claude Code 2.1.295 and later honour, and
153
+ which 2.1.233 ignores.
154
+ - **The shell is judged by its command line, not by its effect.** The baseline
155
+ stops `Read` of `.env`, but `cat .env` in `Bash` is a different call, and
156
+ no path rule sees it. Write shell rules (`command`) for the programs you care
157
+ about. PRAE does not yet screen tool output in Claude Code. A `PostToolUse`
158
+ hook can only replace output in each tool's exact output shape, which this
159
+ release does not take on.
160
+ - **PRAE's own files are protected only against plain attempts.** Writes to the
161
+ pack, the ledger and the hook settings are refused (`PRAE-SELF`), and so are
162
+ reads of the signing key. Shell commands that name the pack, the settings or
163
+ the key are refused too, even read-only ones such as `wc -c <pack>`. But the agent runs as you. A shell can spell a path
164
+ in ways no string check sees, so a determined agent with `Bash` can still
165
+ edit them. An edited ledger breaks its chain and its signatures. A wholesale
166
+ rewrite by someone who holds the key needs an external checkpoint
167
+ (`prae checkpoint`) to catch.
168
+ - **What the hook never sees.** Files you attach with `@` in a prompt are read
169
+ without a tool call, so no hook fires. Managed settings can switch user hooks
170
+ off (`allowManagedHooksOnly`, `disableAllHooks`).
171
+ - **Coverage depends on knowing what a tool does.** Built-in tools map to
172
+ capabilities by name. MCP tools map by their tool name: the reference
173
+ `filesystem` and `fetch` servers' tools, and `prae mcp`'s own, are
174
+ recognised. Any other MCP tool is recorded and matched only by rules that
175
+ name the tool. `WebSearch` has a query, not a URL, so host rules do not
176
+ apply to it. A path that only appears inside an array (for example
177
+ `read_multiple_files`) is not path-checked.
178
+ - **What happens after a hold is not recorded.** The row says the call was
179
+ held (`ask`). Whether you then approved it in the prompt is not written
180
+ back, because a `PreToolUse` hook gets no outcome. The report lists every
181
+ enforced hold under "awaiting human".
182
+ - **The agent's own account is not evidence.** In the verification runs the
183
+ model twice reported a refused call as having run. The ledger, Claude
184
+ Code's `permission_denials` and the file system agreed with each other, and
185
+ they are what to read.
186
+ - **Budgets cost a ledger read.** A pack with a `count` rule makes each call
187
+ count this session's earlier rows in the ledger. Packs without one never
188
+ read the ledger.
189
+ - **Latency.** Each tool call starts one node process. Measured on an M-series
190
+ Mac, the built `lib/bin.js` took 30 ms at p50 and 34 ms at p95. Run from
191
+ `src/bin.ts` with Node's type stripping, it took 60 ms at p50 and 64 ms at
192
+ p95. A bare `node -e 0` takes 20 ms on the same machine.