Skip to content

Commit b69d167

Browse files
bminierclaude
andcommitted
docs: post-v0.6 sync — audit-log security invariants, refusal UX, threat model
`/document-release` sweep over the v0.6 ship (PRs #178 + #184). The audit-log architecture doc, CLI reference, undo/redo user guide, and the security page all had drift relative to what shipped — the six codex passes + Claude code-review pass + security review added behaviors that the docs hadn't caught up to. Updates: - `docs/src/architecture/audit-log.md`: new "Security invariants" section documenting the path allowlist (#183), degraded-log refusal (#170), and tail-ID stalecheck (#165/#171). Also bumped the stale `claude_scope_version` example from 0.3.0 to 0.6.0 in the record-shape snippet. - `docs/src/security.md`: new "Threat model" section covering hostile audit-log entries (linking to the allowlist invariants) and hand-edited settings files (linking to atomic-writes), plus explicit out-of-scope items (multi-user systems, resource exhaustion, binary tampering). - `docs/src/user-guide/undo-redo.md`: documented the three new user-visible refusals — the "log degraded" tooltip / button disable, the "log changed since the preview" staleness error, and the "path injection refused" hostile-record message. - `docs/src/reference/cli.md`: new "Refusals" subsection covering the same three error states for `undo` / `redo` / `restore`, including the explicit note that `--yes` does not bypass any of them. README.md already had v0.6 feature coverage (audit log bullet + keybindings table entry) from earlier commits; no further edits. No source-code TODOs are stale — the audit/restore code is comment- dense but every comment names a live invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 78ab87b commit b69d167

4 files changed

Lines changed: 126 additions & 1 deletion

File tree

docs/src/architecture/audit-log.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ record valid.
3636
},
3737
"path": ["permissions", "allow", 0],
3838
"to_kind": null, // set on change-kind ops
39-
"claude_scope_version": "0.3.0"
39+
"claude_scope_version": "0.6.0"
4040
}
4141
```
4242

@@ -166,6 +166,45 @@ to a point that lives in an archive works the same way as restoring
166166
to a point in the active log — same `record_sides` walk, same per-
167167
file snapshot revert.
168168

169+
## Security invariants
170+
171+
The audit log is read by every undo / redo / restore call, and its
172+
contents drive direct writes to the user's settings files. A hostile
173+
line appended to `audit.jsonl` (cloud-sync collision, malicious
174+
postinstall, compromised tool with user-scope write) must not be able
175+
to escalate into arbitrary file-write. Three gates enforce that.
176+
177+
**Path allowlist (#183).** Every audit-log boundary —
178+
`undo_redo_target`, `restore_to_point_preview`,
179+
`apply_restore_to_point`, and the CLI's `cmd_undo` / `cmd_redo` /
180+
`cmd_restore` — calls `validate_audit_records` immediately after
181+
reading records and before building any `RestorePlan`. Each
182+
`Side.file_path` must equal one of the four resolved `ScopePaths`
183+
slots (`local`, `project`, `user_local`, `user`) for that record's
184+
own `project_dir`, against the active home override. Canonicalization
185+
absorbs platform-specific path forms (`/var → /private/var` on macOS,
186+
`\\?\C:\…` extended-length paths on Windows). The basename must be
187+
`settings.json` or `settings.local.json`. Any path outside the
188+
allowlist is refused with a clear `path injection refused` message;
189+
no writes happen.
190+
191+
**Degraded-log refusal (#170).** `audit::read_all` reports a count
192+
of unreadable lines (corrupt JSON, schema drift the reader can't
193+
parse, truncated tails). Every undo / redo / restore path gates on
194+
that count: the GUI's `require_clean_audit_log` and the CLI's
195+
`read_audit_log` both refuse with a non-zero exit / blocked topbar
196+
button when `skipped > 0`. A partial log could leave the undo/redo
197+
state machine pointing at an op that was already undone, and
198+
confirming would emit a duplicate `restore` entry.
199+
200+
**Tail-ID stalecheck (#165, #171).** Every restore re-reads the log
201+
immediately before applying and compares the trailing record's ULID
202+
against the value captured at preview time. A mismatch means the log
203+
changed under the user — refuse rather than apply a plan against a
204+
log the user didn't see. This catches concurrent GUI/CLI appends in
205+
the prompt window AND the (smaller) window between the initial read
206+
and `--yes` apply.
207+
169208
## Sandbox
170209

171210
When `--home` is set, `emit_audit` skips the rotation + append

docs/src/reference/cli.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,26 @@ For `undo` / `redo` / `restore`, `--json` emits the `RestorePreview`
137137
for a `--dry-run` and the resulting `restore` entry (as an
138138
`AuditRecordView`, the same shape `history --json` produces) once
139139
applied.
140+
141+
### Refusals
142+
143+
`undo` / `redo` / `restore` exit non-zero with a clear message and
144+
write nothing in these cases:
145+
146+
- **`N audit-log entries are unreadable — refusing to compute
147+
undo/redo against a partial log.`** The audit log has malformed
148+
lines that the reader skipped. Acting against a partial log could
149+
emit a duplicate restore entry; repair the log (copy aside, drop
150+
the broken lines) and retry.
151+
- **`the audit log changed since the plan was built` /
152+
`the audit log changed while waiting for confirmation`.** A
153+
concurrent CLI or GUI session appended to the log between this
154+
command's plan-build and apply (or during the confirm prompt). The
155+
stale plan is rejected; re-run the command against the fresh log.
156+
- **`path injection refused`.** An entry's `file_path` points outside
157+
the legitimate scope set for its `project_dir`. Indicates the log
158+
has been hand-edited or corrupted by a third party; do not apply.
159+
See the [security threat model](../security.md#threat-model).
160+
161+
These refusals apply equally under `--yes` — there is no path that
162+
silently writes to the wrong file.

docs/src/security.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,44 @@ tomorrow with no code change on our end.
6161
documented at [Architecture → Audit log](./architecture/audit-log.md).
6262
- The docs site (this one) has **no analytics**. No tracking
6363
pixels, no third-party scripts.
64+
65+
## Threat model
66+
67+
ClaudeScope is a single-user desktop tool. The user running the app
68+
is the only legitimate operator. Within that frame, two threat
69+
surfaces are explicitly defended:
70+
71+
**Hostile audit-log entries.** An attacker who can append a line to
72+
`~/.claude/claude-scope/audit.jsonl` (cloud-sync collision from a
73+
compromised peer, a previously-compromised tool with user-scope
74+
write, a malicious package postinstall, a shared workstation) should
75+
not be able to escalate that capability. The audit log drives
76+
`apply_restore_plan` to write to files at paths recorded in the log
77+
itself — without defenses, one well-formed line could direct
78+
ClaudeScope to write attacker-controlled JSON to any path the user
79+
can write to. The
80+
[Architecture → Audit log § Security invariants](./architecture/audit-log.md#security-invariants)
81+
section documents the three gates (path allowlist, degraded-log
82+
refusal, tail-ID stalecheck) that enforce containment. The path
83+
allowlist is the load-bearing control: every `Side.file_path` is
84+
validated against the four legitimate `ScopePaths` for its
85+
`project_dir` before any write happens.
86+
87+
**Hand-edited settings files.** Users (or other tools) may edit
88+
`settings.json` outside ClaudeScope at any time. The atomic-write
89+
path captures a `FileStamp` at load time and refuses the save when
90+
the stamp differs at persist time — the user's external edit
91+
survives. See
92+
[Architecture → Atomic writes](./architecture/atomic-writes.md) for
93+
the contract.
94+
95+
Out of scope:
96+
97+
- Multi-user systems with adversarial co-users. The threat model
98+
assumes a single trusted user; on shared workstations, OS-level
99+
ACLs are the right layer.
100+
- Resource-exhaustion attacks. A user who can write large amounts
101+
of data into `audit.jsonl` can fill the disk; that's a property
102+
of any append-only log, not a ClaudeScope-specific issue.
103+
- Tampering with the bundled binary. Binary integrity is the
104+
installer / OS package manager's responsibility.

docs/src/user-guide/undo-redo.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,28 @@ captured. If the file was hand-edited since (so its current contents
6969
differ from what the log expected), the confirm modal shows a warning
7070
band — you can still proceed, but you'll be overwriting that edit.
7171

72+
**Disabled with a "log degraded" tooltip.** If
73+
`~/.claude/claude-scope/audit.jsonl` has unreadable lines (corrupt
74+
JSON, a crash mid-write, schema drift the build doesn't recognize),
75+
both Undo and Redo are withheld until the log is repaired. The
76+
tooltip names the count of unreadable entries. Acting against a
77+
partial log could emit a duplicate restore entry, so ClaudeScope
78+
refuses rather than guess. The CLI exits non-zero with the same
79+
message. Repair option: copy `audit.jsonl` aside, drop the broken
80+
lines, restart.
81+
82+
**"Log changed since the preview."** If a concurrent CLI or GUI
83+
session writes to the audit log while the confirm modal is open,
84+
the apply is refused with that message — re-open the History view
85+
and retry against the fresh state. Same posture under
86+
`claude-scope-cli undo` and `--yes`.
87+
88+
**"Path injection refused."** If an audit-log entry's `file_path`
89+
points outside the legitimate scope set for its project (e.g. an
90+
attacker hand-wrote a hostile line into `audit.jsonl`), the restore
91+
is refused before any I/O. See the
92+
[Security threat model](../security.md#threat-model) for context.
93+
7294
## Restore to before an entry
7395

7496
Single-step undo walks back one operation at a time. To jump back

0 commit comments

Comments
 (0)