flatbread-cli 0.0.0-stage → 0.1.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 +370 -3
- package/dist/flatbread.mjs +12300 -0
- package/package.json +33 -4
package/README.md
CHANGED
|
@@ -1,3 +1,370 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# flatbread-cli
|
|
2
|
+
|
|
3
|
+
Reads a completed agent session off local disk, normalizes it into a
|
|
4
|
+
`FlatbreadTrace`, and sanitizes it for sharing. Nothing is uploaded unless you pass `--publish`
|
|
5
|
+
(or set `FLATBREAD_API_URL` and `FLATBREAD_API_TOKEN` yourself), and then only after the review is
|
|
6
|
+
confirmed. The commands still to come (`setup`, `blame`) are in `PLAN.md` §8.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g flatbread-cli # installs the `flatbread` command (Node 20 or newer)
|
|
10
|
+
npx -y flatbread-cli --help # or run it without installing
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Not on npm until the owner runs the first `npm publish`** - until then, `npm pack` (below) and
|
|
14
|
+
`npm install -g ./flatbread-cli-<version>.tgz`. **The npm package is `flatbread-cli`, not `flatbread`.** `flatbread` on npm is an unrelated
|
|
15
|
+
project, so `npx flatbread` runs somebody else's code. The command it installs is `flatbread`.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
flatbread parse --file <path.jsonl> # parse one session log
|
|
21
|
+
flatbread parse --latest # newest primary session under the projects dir
|
|
22
|
+
|
|
23
|
+
flatbread share # newest session: parse, scan, review, write a draft
|
|
24
|
+
flatbread share --file <path.jsonl> # a specific session
|
|
25
|
+
flatbread share --no-review # redact everything, no browser (non-interactive)
|
|
26
|
+
flatbread share --no-open # start the review server but do not launch a browser
|
|
27
|
+
|
|
28
|
+
flatbread share --publish # after review, upload to the signed-in site
|
|
29
|
+
|
|
30
|
+
flatbread login | logout | whoami # sign the CLI in to a site (device flow)
|
|
31
|
+
|
|
32
|
+
flatbread share --pr <url> # link the pull request this run belongs to (https)
|
|
33
|
+
|
|
34
|
+
flatbread view # write the Execution Brief and CAPSULE.md beside the newest artifact
|
|
35
|
+
|
|
36
|
+
flatbread pull <id-or-url> # print a published run's Context Capsule
|
|
37
|
+
flatbread pull <id-or-url> --out <f> # or write it to a file
|
|
38
|
+
flatbread mcp # stdio MCP server: flatbread_search_traces, flatbread_get_capsule
|
|
39
|
+
|
|
40
|
+
flatbread hook install # offer the share at the end of a Claude Code turn
|
|
41
|
+
flatbread hook status # where it is installed, and what it has seen
|
|
42
|
+
flatbread hook uninstall # remove only the flatbread handlers
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The Claude Code hook
|
|
46
|
+
|
|
47
|
+
`flatbread hook install` merges two handlers into `~/.claude/settings.json`
|
|
48
|
+
(`CLAUDE_CONFIG_DIR` moves it; `--settings <path>` picks any file; `--dry-run` prints the
|
|
49
|
+
merged block and writes nothing). Other tools' hooks in that file are left alone, a second
|
|
50
|
+
install changes nothing, and a settings file that does not parse is **refused** - the block
|
|
51
|
+
to paste is printed instead.
|
|
52
|
+
|
|
53
|
+
What the two handlers do is decided by what the harness allows, not by taste:
|
|
54
|
+
|
|
55
|
+
| Event | What it does | Why not more |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `Stop` | prints ONE line offering the share, once per session, after two finished turns | it is the only event whose `systemMessage` reaches the human |
|
|
58
|
+
| `SessionEnd` | records that the session ended, and its reason | Claude Code **discards** a SessionEnd hook's output, so it cannot speak |
|
|
59
|
+
|
|
60
|
+
- The hook **never blocks and never continues a turn**: `decision: "block"` and
|
|
61
|
+
`additionalContext` would put the agent back to work, and exit 2 blocks a `Stop`, so
|
|
62
|
+
`hook notify` exits 0 on every path including its own internal errors. Diagnostics go to
|
|
63
|
+
stderr, which is the debug log (`claude --debug`), and they name every reason it stayed
|
|
64
|
+
quiet.
|
|
65
|
+
- `SessionEnd` asks for **no timeout**, because a per-hook timeout raises the shared
|
|
66
|
+
1.5-second SessionEnd budget for every other hook in that file.
|
|
67
|
+
- The offer **claims nothing**. It does not say the run is unshared - the hook never reads
|
|
68
|
+
the store - and it cannot ask for a visibility, because a hook has no terminal. It names
|
|
69
|
+
the command, and the choice stays in the local review page.
|
|
70
|
+
- **An offer it cannot remember making is not made.** The "offered" record is written before
|
|
71
|
+
the line is printed; if that write fails the hook stays quiet rather than nag every turn.
|
|
72
|
+
- Turn it off with `FLATBREAD_HOOK_DISABLED=1`, or change when it speaks with
|
|
73
|
+
`FLATBREAD_HOOK_OFFER_AFTER=<turns>`. State lives in `~/.flatbread/hook-state.json`
|
|
74
|
+
(`FLATBREAD_HOME` moves it) and is capped at 50 sessions. Nothing is ever written into
|
|
75
|
+
your repository.
|
|
76
|
+
|
|
77
|
+
Two things the harness's own changelog says can break an install silently, both handled:
|
|
78
|
+
|
|
79
|
+
- **Exec form (`args`) landed in Claude Code 2.1.139.** An older version ignores `args` and
|
|
80
|
+
would run the interpreter with no arguments: a hook that never answers, costing every turn
|
|
81
|
+
the full Stop timeout. `hook install` reads `claude --version` and **refuses** on a version
|
|
82
|
+
it knows is too old; a version it could not read is reported as `NOT RETRIEVED`, never as
|
|
83
|
+
"fine".
|
|
84
|
+
- **`transcript_path` pointed at the wrong directory for resumed sessions before 2.1.72**, so
|
|
85
|
+
the offer requires the transcript to EXIST before it names it. A wrong path shows up as
|
|
86
|
+
silence with a reason, never as a command that fails when you paste it.
|
|
87
|
+
|
|
88
|
+
`hook install` also **probes before it writes**: it runs the exact command it is about to
|
|
89
|
+
install, with a synthetic event, and refuses to write one that does not exit 0 silently. That
|
|
90
|
+
closes the worst failure available — a hook that is installed, broken, and only visible under
|
|
91
|
+
`claude --debug`. `--no-probe` skips it for a machine where spawning is blocked.
|
|
92
|
+
|
|
93
|
+
One caveat worth knowing: Claude Code writes the transcript asynchronously, so the last
|
|
94
|
+
message of a turn may not be in the file yet. The offer names a command you run later, and the
|
|
95
|
+
trace is parsed then, so this only matters if you share within a second of the answer.
|
|
96
|
+
|
|
97
|
+
**It has never run under a real Claude Code**: this machine has none. Everything above is
|
|
98
|
+
proven by unit tests against recorded event payloads and by driving `hook notify` with the
|
|
99
|
+
event JSON on stdin. `HUMAN-TESTS.md` **A7** is the test a person has to run.
|
|
100
|
+
|
|
101
|
+
## Persistence and publishing
|
|
102
|
+
|
|
103
|
+
After the review gate, `share` mints a slug and writes a `TraceRecord` into the local
|
|
104
|
+
store (`packages/cli/.tmp/store/<slug>.json`), then prints:
|
|
105
|
+
|
|
106
|
+
- the **slug** and its `/r/<slug>` URL
|
|
107
|
+
- a **manage token**, shown once. Only its sha256 hash is stored, so there is no
|
|
108
|
+
recovery path; a `private` record is unreachable without it.
|
|
109
|
+
|
|
110
|
+
`FLATBREAD_STORE_DIR` overrides the store location outright; `FLATBREAD_TMP_DIR`
|
|
111
|
+
moves the whole scratch directory (the store becomes `<tmp>/store`).
|
|
112
|
+
|
|
113
|
+
**Nothing is uploaded** unless you pass `--publish` with a login stored by
|
|
114
|
+
`flatbread login`, or set BOTH `FLATBREAD_API_URL` and `FLATBREAD_API_TOKEN`. The URL
|
|
115
|
+
and the token resolve independently, each falling back to the stored login, and a
|
|
116
|
+
STORED token is never offered to a site that did not issue it. Being signed in is not
|
|
117
|
+
permission to upload: that is what `--publish` is for. `packages/cli/src/publish.ts` is the only file in this repository
|
|
118
|
+
permitted to make an outbound request, the verify suite enforces that by name across
|
|
119
|
+
all four packages, and it refuses to send a token over plain `http` to anything but
|
|
120
|
+
loopback. A publish failure never invalidates the local record; it is reported.
|
|
121
|
+
|
|
122
|
+
Visibility is `private` unless a human raises it - in the review UI, or from the owner
|
|
123
|
+
panel on the site while signed in as the run's owner. There is no `--visibility` flag
|
|
124
|
+
and no API field for it: `--no-review` is non-interactive and must never publish.
|
|
125
|
+
|
|
126
|
+
`parse` writes `packages/cli/.tmp/last-parse.json`.
|
|
127
|
+
`share` writes `packages/cli/.tmp/last-share.json` and refreshes `last-parse.json`.
|
|
128
|
+
`view` reads the newest artifact (share draft preferred over raw parse), builds the
|
|
129
|
+
deterministic Execution Brief with `buildExecutionBrief`, writes `last-brief.json` and
|
|
130
|
+
`CAPSULE.md`, and prints `open http://127.0.0.1:3000/r/local`. It does **not**
|
|
131
|
+
start Next: a hung dev server is worse than one honest instruction line. If the source
|
|
132
|
+
is a raw parse, or a share draft that kept flagged originals, it prints a NOTE - the
|
|
133
|
+
capsule quotes trace text verbatim and inherits whatever the source still contains.
|
|
134
|
+
|
|
135
|
+
`parse` and `share` record the git branch, HEAD commit and uncommitted file names when the
|
|
136
|
+
session's working directory is a git repository. Nothing else is read from git, the index is
|
|
137
|
+
never written, and a log that names a different branch than the checkout gets no commit.
|
|
138
|
+
|
|
139
|
+
`pull` and `mcp` only read. A token from `flatbread login` goes only to the site that issued it;
|
|
140
|
+
a private run's share link (`?t=`) is sent as a header, never in a URL. Add the MCP server to
|
|
141
|
+
Claude Code with `claude mcp add flatbread -- flatbread mcp`.
|
|
142
|
+
|
|
143
|
+
### Building the package
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm run build -w flatbread-cli # bundles schema, store and zod into dist/flatbread.mjs
|
|
147
|
+
cd packages/cli && npm pack # flatbread-cli-<version>.tgz, the file npm publish uploads
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Verify
|
|
151
|
+
|
|
152
|
+
From the repo root:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npm install
|
|
156
|
+
npx tsx packages/cli/src/index.ts parse --file packages/cli/test/fixtures/claude-code-sample.jsonl
|
|
157
|
+
npm run verify # every suite, no framework, no network
|
|
158
|
+
npm run typecheck
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`parse --latest` on a machine with no `~/.claude/projects` prints
|
|
162
|
+
`no sessions found in <dir>` and exits 0. To prove it without deleting anything:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# macOS / Linux
|
|
166
|
+
CLAUDE_CONFIG_DIR=$(mktemp -d) npx tsx packages/cli/src/index.ts parse --latest
|
|
167
|
+
# PowerShell
|
|
168
|
+
$env:CLAUDE_CONFIG_DIR="$env:TEMP\no-claude-here"; npx tsx packages/cli/src/index.ts parse --latest
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Requires Node 20+. Nothing is compiled: `tsx` runs the TypeScript directly, and
|
|
172
|
+
`@flatbread/schema` is consumed as source through the workspace symlink.
|
|
173
|
+
|
|
174
|
+
## Where sessions live
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
<projects-dir>/<encoded-cwd>/<session-uuid>.jsonl
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- Projects dir is `$CLAUDE_CONFIG_DIR/projects` when that variable is set,
|
|
181
|
+
otherwise `path.join(os.homedir(), ".claude", "projects")`.
|
|
182
|
+
- Encoded cwd replaces every non-alphanumeric character with `-`. The encoding is
|
|
183
|
+
lossy, so it is only ever used in the encode direction, never decoded back.
|
|
184
|
+
- `agent-*.jsonl` and anything under a `subagents/` directory is a subagent
|
|
185
|
+
transcript and is never treated as a primary session.
|
|
186
|
+
|
|
187
|
+
## Reading rules
|
|
188
|
+
|
|
189
|
+
The reader streams the file with `readline` and parses one line at a time. A
|
|
190
|
+
whole session is never handed to `JSON.parse`, so one corrupt byte cannot cost
|
|
191
|
+
the entire trace.
|
|
192
|
+
|
|
193
|
+
Three separate skip counters are reported, because they mean different things:
|
|
194
|
+
|
|
195
|
+
| counter | meaning |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `malformed` | not JSON, not an object, or no string `type`. Corruption. |
|
|
198
|
+
| `known ignored type` | a type we know and deliberately drop (`system`, `progress`, `file-history-snapshot`, `queue-operation`, `pr-link`, …). |
|
|
199
|
+
| `unknown type` | a `type` this parser has never seen. **Watch this number** - it is how we find out the format moved. |
|
|
200
|
+
|
|
201
|
+
Blank lines are counted separately and are not treated as corruption.
|
|
202
|
+
|
|
203
|
+
Only `user` and `assistant` records produce steps. Only these fields are read:
|
|
204
|
+
|
|
205
|
+
- any record: `type`, `uuid`, `parentUuid`, `timestamp`, `sessionId`, `cwd`,
|
|
206
|
+
`gitBranch`, `version`, `isSidechain`
|
|
207
|
+
- `user`: `message.content` as a string, or as blocks of `text` / `tool_result`
|
|
208
|
+
(`tool_use_id`, `is_error`, `content`)
|
|
209
|
+
- `assistant`: `message.model`, `message.content[]` of `text` / `thinking` /
|
|
210
|
+
`tool_use` (`id`, `name`, `input`), `message.usage.{input_tokens,
|
|
211
|
+
output_tokens, cache_creation_input_tokens, cache_read_input_tokens}`
|
|
212
|
+
|
|
213
|
+
## Normalization rules
|
|
214
|
+
|
|
215
|
+
- **Order.** File order is the base truth. Steps are re-sorted by `timestamp`
|
|
216
|
+
only when every step has a parseable one, and that sort is stable, so several
|
|
217
|
+
blocks inside one message keep their block order.
|
|
218
|
+
- **`user_prompt`.** A user message that is *not* only `tool_result` blocks. A
|
|
219
|
+
turn carrying both a prompt and tool results yields both kinds of step, with
|
|
220
|
+
the prompt placed at its first text block.
|
|
221
|
+
- **`tool_call`.** One step per `tool_use`. `content` is a one-line label built
|
|
222
|
+
only from input fields that are present (`command`, `file_path`, `pattern`,
|
|
223
|
+
`url`, `description`, `prompt`, `query`); the full input is on `tool.input`.
|
|
224
|
+
- **`tool_result`.** Matched to its `tool_use` by `tool_use_id`. The match sets
|
|
225
|
+
`tool.output` and `tool.isError` on the earlier `tool_call` step **and** emits
|
|
226
|
+
its own step. Unmatched results are counted and reported.
|
|
227
|
+
- **`error_recovery`.** A `tool_result` with `is_error: true` becomes exactly one
|
|
228
|
+
`error_recovery` step; it is *not* also emitted as `tool_result`, or every
|
|
229
|
+
failure would appear twice. Errors are never inferred from prose.
|
|
230
|
+
- **`filesChanged`.** Unique `file_path` (or `notebook_path`) values from
|
|
231
|
+
`Write` / `Edit` / `MultiEdit` / `NotebookEdit` / `Update` inputs, excluding
|
|
232
|
+
calls whose result came back `is_error: true` - a `Write` that raised `EACCES`
|
|
233
|
+
changed nothing. No path in the log means no file recorded; none is invented.
|
|
234
|
+
Unknown tool names contribute nothing here, because "did this write a file"
|
|
235
|
+
cannot be guessed from a name.
|
|
236
|
+
- **`diff`.** Only when the log actually carries one: an explicit `patch`
|
|
237
|
+
string, `old_string`/`new_string`, or the first entry of MultiEdit `edits`.
|
|
238
|
+
A `Write` has no old side, so it gets no diff.
|
|
239
|
+
- **Truncation.** Step `content` at 4000 chars, every string inside `tool.input`
|
|
240
|
+
at 2000 chars, both with `…[truncated]` appended. Limits live in
|
|
241
|
+
`@flatbread/schema` (`LIMITS`) so the web renderer agrees with the CLI.
|
|
242
|
+
- **`title`.** First real `user_prompt`, whitespace collapsed, max 120 chars.
|
|
243
|
+
Otherwise `"Untitled session"`.
|
|
244
|
+
- **`model`.** First `assistant.message.model`, otherwise `"unknown"`.
|
|
245
|
+
- **`visibility`.** Always `"private"` out of the parser.
|
|
246
|
+
|
|
247
|
+
## Deliberate non-goals in this pass
|
|
248
|
+
|
|
249
|
+
No upload, no localhost review UI, no Next.js. Flatbread does not replay a trace onto
|
|
250
|
+
another codebase, and no copy should ever suggest it does.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
# Sharing: `flatbread share`
|
|
255
|
+
|
|
256
|
+
The command exists to put a human gate in front of the data. It never uploads
|
|
257
|
+
anything, and it cannot: there is no outbound request anywhere in this package,
|
|
258
|
+
and a test greps the sources to keep it that way.
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
parse the session -> scan every string -> review in the browser -> write a sanitized draft
|
|
262
|
+
(you decide, per match)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
1. Parse with the same code `parse` uses. Sharing changes nothing about parsing.
|
|
266
|
+
2. Scan `title`, `summary`, `source.cwd`, `source.sourcePath`, every
|
|
267
|
+
`step.content`, every string inside `tool.input` (recursively), `tool.output`,
|
|
268
|
+
`diff.filePath` and `diff.patch`.
|
|
269
|
+
3. Serve one page on `http://127.0.0.1:4321` (then 4322-4325 if busy).
|
|
270
|
+
4. You tick any match to KEEP its original text; everything else is redacted.
|
|
271
|
+
5. You pick visibility: `private` (default), `unlisted`, `public`.
|
|
272
|
+
6. Confirm writes `packages/cli/.tmp/last-share.json`. Cancel writes nothing.
|
|
273
|
+
|
|
274
|
+
## The redaction engine
|
|
275
|
+
|
|
276
|
+
Deterministic, local, no model. The same trace and the same choices always give a
|
|
277
|
+
byte-identical result, which is what makes the review meaningful: the artifact is
|
|
278
|
+
exactly what you approved.
|
|
279
|
+
|
|
280
|
+
Matched spans are replaced with a stable token such as `«REDACTED:aws_access_key»`.
|
|
281
|
+
|
|
282
|
+
| # | rule id | what it matches | note |
|
|
283
|
+
| --- | --- | --- | --- |
|
|
284
|
+
| 1 | `aws_access_key` | `AKIA[0-9A-Z]{16}` | |
|
|
285
|
+
| 2 | `private_key` | `-----BEGIN … PRIVATE KEY-----` … `-----END … PRIVATE KEY-----` | the whole block is one match |
|
|
286
|
+
| 3 | `github_token` | `gh[pousr]_[A-Za-z0-9_]{20,}` | |
|
|
287
|
+
| 4 | `anthropic_key` | `sk-ant-[A-Za-z0-9\-_]+` | before rule 5, so it cannot double-fire |
|
|
288
|
+
| 5 | `openai_key` | `sk-[A-Za-z0-9]{20,}` | |
|
|
289
|
+
| 6 | `slack_token` | `xox[baprs]-[A-Za-z0-9-]+` | |
|
|
290
|
+
| 7 | `jwt` | `eyJ….….…` | before rule 8 |
|
|
291
|
+
| 8 | `bearer_token` | `Bearer <value>` | keeps `Bearer `, redacts the value |
|
|
292
|
+
| 9 | `home_path` | `C:\Users\<name>`, `/Users/<name>`, `/home/<name>` | keeps the path shape, redacts the username; case-insensitive |
|
|
293
|
+
| 10 | `assigned_secret` | `(api_key\|apikey\|api-key\|secret\|password\|token\|passwd)\s*[:=]\s*\S+` | keeps the left-hand name, redacts the value; case-insensitive, because real config says `API_KEY=` |
|
|
294
|
+
| 11 | `secret_field_name` | any string whose FIELD NAME is a credential name | **evaluated first**, see below |
|
|
295
|
+
|
|
296
|
+
**Evaluation order is significant.** An earlier rule wins an overlap, so specific
|
|
297
|
+
patterns come before general ones and no span is ever rewritten twice. The order is
|
|
298
|
+
`secret_field_name` first, then rules 1-10 in the order above — the table is
|
|
299
|
+
numbered to match the spec, not the evaluation sequence.
|
|
300
|
+
|
|
301
|
+
Rule 11 going first is deliberate: a field literally named `password` or
|
|
302
|
+
`authorization` holds a credential in its entirety, so redacting the whole value
|
|
303
|
+
beats redacting the one recognisable substring inside it. The only cost is
|
|
304
|
+
reporting granularity — a Slack token in a field named `token` is reported once as
|
|
305
|
+
`secret_field_name` rather than as `slack_token`.
|
|
306
|
+
|
|
307
|
+
**Rule 11 is an addition beyond the ten text patterns**, and the only rule that
|
|
308
|
+
looks at where a string lives rather than at the string itself. An
|
|
309
|
+
`Authorization: Opaque a7f3c9e1d5b28046` value, or a nested
|
|
310
|
+
`env: { API_KEY: "9c8b7a…" }`, has no recognisable shape — an opaque credential
|
|
311
|
+
looks like nothing — so no text pattern can catch it. Field names that trigger it
|
|
312
|
+
are listed in `SECRET_FIELD_NAMES` in `src/redact.ts`; delete that set and the
|
|
313
|
+
rule id to remove the behaviour.
|
|
314
|
+
|
|
315
|
+
**`bearer_token` matches capital-B `Bearer` only**, as specified. RFC 7235 says the
|
|
316
|
+
scheme is case-insensitive, so a lowercase `bearer <opaque-value>` would be missed
|
|
317
|
+
by this rule; a JWT after it is still caught by rule 7, which runs earlier.
|
|
318
|
+
|
|
319
|
+
**The word "token" in prose is not a secret.** Rule 10 requires a `:` or `=` after
|
|
320
|
+
the name, so "Token usage is high" is untouched. There is a test for exactly that.
|
|
321
|
+
|
|
322
|
+
### What the report does NOT contain
|
|
323
|
+
|
|
324
|
+
`last-share.json` holds the sanitized trace plus a `redaction` report. For every
|
|
325
|
+
match it records the id, rule, field path, offsets, replacement, whether it was
|
|
326
|
+
redacted, and the original's LENGTH — but **not the original text**. A report that
|
|
327
|
+
recorded every original would hand back every secret in the same file that just
|
|
328
|
+
removed them. A short `keptPreview` survives only for matches you chose to keep,
|
|
329
|
+
where the text is in the trace anyway.
|
|
330
|
+
|
|
331
|
+
## The review page
|
|
332
|
+
|
|
333
|
+
One HTML document. Inline CSS, ~25 lines of inline JS, no CDN, no fonts, no
|
|
334
|
+
`<link>`, no `<iframe>`, served with `Content-Security-Policy: default-src 'none'`.
|
|
335
|
+
An offline machine can run the whole review.
|
|
336
|
+
|
|
337
|
+
- **Top:** title, model, step / tool-call / file / error counts, duration, and the
|
|
338
|
+
number of matches.
|
|
339
|
+
- **Main:** a `trace` block (title, summary, cwd, sourcePath) then every step, with
|
|
340
|
+
each matched span marked. The mark shows the replacement token by default and
|
|
341
|
+
the original when you tick KEEP.
|
|
342
|
+
- **Side:** every match with a rule label, a preview of the original capped at 24
|
|
343
|
+
characters, and the field path. `redact all` / `keep all` buttons, the visibility
|
|
344
|
+
radio, and Confirm / Cancel.
|
|
345
|
+
|
|
346
|
+
The page shows unredacted secrets when you tick KEEP, by design — you cannot
|
|
347
|
+
review what you cannot see. That is why it binds `127.0.0.1` only and is never
|
|
348
|
+
written to disk.
|
|
349
|
+
|
|
350
|
+
**The submitted checkbox means KEEP, not redact.** HTML only submits ticked boxes,
|
|
351
|
+
so a value lost in the browser has to mean "redact it". The sidebar still reads
|
|
352
|
+
"redact, on by default" via CSS, but the transport fails closed.
|
|
353
|
+
|
|
354
|
+
## Verify the share path
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npm run verify # includes the redaction / review / share suites
|
|
358
|
+
|
|
359
|
+
# non-interactive, writes a draft with everything redacted
|
|
360
|
+
npx tsx packages/cli/src/index.ts share --file packages/cli/test/fixtures/secrets-sample.jsonl --no-review
|
|
361
|
+
|
|
362
|
+
# the real thing
|
|
363
|
+
npx tsx packages/cli/src/index.ts share --file packages/cli/test/fixtures/secrets-sample.jsonl
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`test/fixtures/secrets-sample.jsonl` carries one of every class: an AWS key id, an
|
|
367
|
+
Anthropic key, a GitHub token, a Slack token, a JWT in a Bearer header, a PEM
|
|
368
|
+
private key, `password=` on one line, Windows and Linux home paths, opaque values
|
|
369
|
+
in credential-named fields, and the prose "Token usage is high" that must survive.
|
|
370
|
+
It contains no real secret.
|