Skip to content

Commit fff59e8

Browse files
Redact Codex reasoning content from retained raw stdout
Normalized events and reports already redacted reasoning items; the retained raw-stdout.log run artifact still carried the provider's reasoning text verbatim. Retention now happens after parsing and replaces each reasoning item's text with a length marker, matching the Ollama adapter's thinking redaction — retained raw streams keep only safe status metadata for reasoning. Caught by the final-verification artifact scan; covered by the strengthened codex events test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 94fd7e4 commit fff59e8

6 files changed

Lines changed: 62 additions & 6 deletions

File tree

integrations/claude-code-plugin/specbridge/dist/checksums.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
"bytes": 153474
88
},
99
"cli.cjs": {
10-
"sha256": "1f1551f92dc99853fed30301ef845dd3dae61faa847170e0f09932ee5861b98b",
11-
"bytes": 2391312
10+
"sha256": "5f9792d574e8964d8f9a76469ef3304db3da595d000382ff1d6b2815a0b05acf",
11+
"bytes": 2392021
1212
},
1313
"mcp-server.cjs": {
1414
"sha256": "a817ad7e791bd2a40296cc2543cb4d38e05dd6ed13be9f59396280a859ebcee0",

integrations/claude-code-plugin/specbridge/dist/cli.cjs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39204,6 +39204,21 @@ function itemPayload(item) {
3920439204
}
3920539205
return payload;
3920639206
}
39207+
function redactCodexStdoutForRetention(stdout) {
39208+
return stdout.split(/\r?\n/).map((line) => {
39209+
const trimmed = line.trim();
39210+
if (!trimmed.startsWith("{") || !trimmed.includes('"reasoning"')) return line;
39211+
try {
39212+
const parsed = JSON.parse(trimmed);
39213+
if (parsed.item?.type === "reasoning" && typeof parsed.item.text === "string") {
39214+
parsed.item.text = `[redacted reasoning: ${parsed.item.text.length} chars]`;
39215+
return JSON.stringify(parsed);
39216+
}
39217+
} catch {
39218+
}
39219+
return line;
39220+
}).join("\n");
39221+
}
3920739222
function normalizeCodexEvents(stream, context, timestamp) {
3920839223
const normalized = [];
3920939224
const push = (type, providerEventType, payload) => {
@@ -39554,7 +39569,9 @@ var CodexCliRunner = class {
3955439569
const usage = usageFromStream(stream, processResult.observation.durationMs, this.config.model);
3955539570
const base = {
3955639571
runner: this.name,
39557-
rawStdout: processResult.stdout,
39572+
// Parsing already happened on the pristine stream; the RETAINED bytes
39573+
// carry only safe status metadata for reasoning items.
39574+
rawStdout: redactCodexStdoutForRetention(processResult.stdout),
3955839575
rawStderr: processResult.stderr,
3955939576
process: processResult.observation,
3956039577
durationMs: Math.max(0, Date.now() - started),

packages/runners/src/codex-cli/events.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,33 @@ function itemPayload(item: z.infer<typeof codexItemSchema>): Record<string, stri
172172
return payload;
173173
}
174174

175+
/**
176+
* Redact reasoning content from a raw JSONL stdout stream before it is
177+
* retained as a run artifact. Parsing happens BEFORE this: the retained
178+
* bytes keep every event's structure (and non-reasoning content) but
179+
* reasoning text is replaced with a length marker — raw retention keeps
180+
* only safe status metadata for reasoning, matching the Ollama adapter.
181+
*/
182+
export function redactCodexStdoutForRetention(stdout: string): string {
183+
return stdout
184+
.split(/\r?\n/)
185+
.map((line) => {
186+
const trimmed = line.trim();
187+
if (!trimmed.startsWith('{') || !trimmed.includes('"reasoning"')) return line;
188+
try {
189+
const parsed = JSON.parse(trimmed) as { item?: { type?: string; text?: string } };
190+
if (parsed.item?.type === 'reasoning' && typeof parsed.item.text === 'string') {
191+
parsed.item.text = `[redacted reasoning: ${parsed.item.text.length} chars]`;
192+
return JSON.stringify(parsed);
193+
}
194+
} catch {
195+
// Not JSON — leave the line untouched.
196+
}
197+
return line;
198+
})
199+
.join('\n');
200+
}
201+
175202
/**
176203
* Normalize parsed Codex events. Reasoning items become `message.completed`
177204
* with a redacted payload — their content is intentionally absent.

packages/runners/src/codex-cli/runner.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ import {
4141
runCodexInvocation,
4242
} from './invocation.js';
4343
import type { CodexEventStream } from './events.js';
44-
import { normalizeCodexEvents, parseCodexEventStream } from './events.js';
44+
import {
45+
normalizeCodexEvents,
46+
parseCodexEventStream,
47+
redactCodexStdoutForRetention,
48+
} from './events.js';
4549

4650
/**
4751
* Codex CLI runner (v0.6): invokes the locally installed `codex` CLI in
@@ -413,7 +417,9 @@ export class CodexCliRunner implements AgentRunner {
413417
const usage = usageFromStream(stream, processResult.observation.durationMs, this.config.model);
414418
const base = {
415419
runner: this.name,
416-
rawStdout: processResult.stdout,
420+
// Parsing already happened on the pristine stream; the RETAINED bytes
421+
// carry only safe status metadata for reasoning items.
422+
rawStdout: redactCodexStdoutForRetention(processResult.stdout),
417423
rawStderr: processResult.stderr,
418424
process: processResult.observation,
419425
durationMs: Math.max(0, Date.now() - started),

packages/runners/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export {
6060
export {
6161
parseCodexEventStream,
6262
normalizeCodexEvents,
63+
redactCodexStdoutForRetention,
6364
MAX_RETAINED_EVENTS,
6465
type CodexEvent,
6566
type CodexEventStream,

tests/runners/codex-cli.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ describe('codex authoring (read-only boundary)', () => {
233233
expect(existsSync(path.join(workspaceRoot, 'rogue-authoring-write.txt'))).toBe(false);
234234
});
235235

236-
it('normalizes machine-readable events and redacts reasoning content', async () => {
236+
it('normalizes machine-readable events and redacts reasoning content everywhere', async () => {
237237
withScenario('success');
238238
const runner = new CodexCliRunner(fakeCodexConfig());
239239
const { workspaceRoot, runDir } = scratchDirs();
@@ -252,6 +252,11 @@ describe('codex authoring (read-only boundary)', () => {
252252
(event) => event.providerEventType === 'item.completed:reasoning',
253253
);
254254
expect(reasoning?.payload['redacted']).toBe(true);
255+
// Reasoning content never survives into the RETAINED raw stream either —
256+
// only a length marker does (the non-reasoning events stay intact).
257+
expect(result.rawStdout).not.toContain('REASONING-SECRET-DO-NOT-EXPOSE');
258+
expect(result.rawStdout).toContain('[redacted reasoning:');
259+
expect(result.rawStdout).toContain('thread.started');
255260
});
256261

257262
it('malformed final output fails safely', async () => {

0 commit comments

Comments
 (0)