review fixes: block Codeman's own credential-bearing JSON, make the inside-anchor test bite

Widening the servable extensions to EDITABLE_EXTENSIONS made ~/.codeman
JSON previewable for the first time, and the blocklist named only
state.json. But settings.json holds a credential BY SCHEMA
(voiceSettings.apiKey), push-keys.json holds the VAPID PRIVATE key, and
intents.json is written 0600 precisely because captured prompts can carry
secrets — all three were one authenticated click away once an agent
printed the path. Blocked alongside state.json, whose rule now also
catches state-* siblings.

The never-re-cuts-inside-an-anchor test used an unmatchable URL tail, so
it passed with the guard deleted; the fixture now carries a matchable
/tmp path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-16 19:21:43 +02:00
parent da999b130e
commit ea4b940cef
3 changed files with 30 additions and 8 deletions
+17 -6
View File
@@ -80,12 +80,23 @@ const SENSITIVE_PATTERNS: RegExp[] = [
/\/\.claude\/\.credentials\.json$/,
/\/\.codeman[^/]*\/hook-secret$/,
/\/\.codeman[^/]*\/users\.json$/,
// Codeman's own state file. Named once `.json` became previewable outside the
// workspace: `SessionState.envOverrides` persists whatever the user set for a
// session, and the env allowlist admits key-shaped names (`GEMINI_API_KEY`,
// `CLAUDE_CODE_*`), so this file can hold a live credential. Same reasoning
// as the two entries above, and it leaves the rest of ~/.codeman attachable.
/\/\.codeman[^/]*\/state\.json$/,
// Codeman's own state files. Named once `.json` became previewable outside
// the workspace: `SessionState.envOverrides` persists whatever the user set
// for a session, and the env allowlist admits key-shaped names
// (`GEMINI_API_KEY`, `CLAUDE_CODE_*`), so state can hold a live credential.
// `state[^/]*` rather than `state`: siblings like state-inner.json carry the
// same payload. Same reasoning as the two entries above, and it leaves the
// rest of ~/.codeman attachable.
/\/\.codeman[^/]*\/state[^/]*\.json$/,
// settings.json holds a credential BY SCHEMA (`voiceSettings.apiKey`, the
// Deepgram key); push-keys.json holds the VAPID PRIVATE key (enough to forge
// push notifications to every subscribed device); intents.json is written
// 0600 precisely because captured prompts can contain secrets, and is
// deliberately kept out of /api/search — it must not be readable through a
// different route instead.
/\/\.codeman[^/]*\/settings\.json$/,
/\/\.codeman[^/]*\/push-keys\.json$/,
/\/\.codeman[^/]*\/intents\.json$/,
];
/**
+5 -2
View File
@@ -100,11 +100,14 @@ describe('response viewer file-path linkifier', () => {
it('never re-cuts text already inside an anchor', () => {
// marked autolinks URLs; a path-looking tail inside one must stay whole, and
// a nested <a> is invalid markup that would swallow the outer link's click.
const root = linkify('<p><a href="https://example.com/x/y.png">https://example.com/x/y.png</a></p>');
// ⚠️ The URL's tail MUST be a string the pattern matches on its own
// (`/tmp/...` here): with an unmatchable tail this test passes with the
// inside-anchor guard deleted, i.e. it pins nothing.
const root = linkify('<p><a href="https://example.com/tmp/shot.png">https://example.com/tmp/shot.png</a></p>');
expect(paths(root)).toHaveLength(0);
expect(root.querySelectorAll('a')).toHaveLength(1);
expect(root.querySelector('a')!.getAttribute('href')).toBe('https://example.com/x/y.png');
expect(root.querySelector('a')!.getAttribute('href')).toBe('https://example.com/tmp/shot.png');
});
it('leaves text with no path untouched', () => {
+8
View File
@@ -79,6 +79,14 @@ describe('isSensitivePath', () => {
// workspace.
['codeman state file', `${HOME}/.codeman/state.json`],
['codeman state file on a named instance', `${HOME}/.codeman-beta/state.json`],
['codeman state sibling (same payload)', `${HOME}/.codeman/state-inner.json`],
// settings.json holds voiceSettings.apiKey by schema; push-keys.json holds
// the VAPID PRIVATE key; intents.json is 0600 because captured prompts can
// contain secrets and is deliberately kept out of /api/search.
['codeman settings (Deepgram key)', `${HOME}/.codeman/settings.json`],
['codeman push keys (VAPID private)', `${HOME}/.codeman/push-keys.json`],
['codeman intent profiles', `${HOME}/.codeman/intents.json`],
['codeman intents on a named instance', `${HOME}/.codeman-beta/intents.json`],
];
it.each(blocked)('blocks the %s', (_label, path) => {