Skip to content

Commit 397a118

Browse files
committed
Harden redacted share prompt handling
1 parent 4ed5c0e commit 397a118

5 files changed

Lines changed: 120 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Unreleased
44

55
### Added (CLI)
6-
- **Redacted share bundles.** New `codeburn share` command writes a local JSON support bundle with project/session/turn structure while pseudonymizing project labels and redacting common emails, local paths, credentials, bearer tokens, and API keys. Supports period, date range, provider, project, exclude, and output-path filters.
6+
- **Redacted share bundles.** New `codeburn share` command writes a local JSON support bundle with project/session/turn structure while pseudonymizing project labels and redacting common emails, local paths, credentials, bearer tokens, and API keys. Prompt text is omitted by default and can be explicitly included with `--include-prompts`. Supports period, date range, provider, project, exclude, and output-path filters.
77
- **Multiple subscription plans can be tracked at the same time.**
88
`codeburn plan set` now stores plans in a provider-keyed `plans` map, so
99
setting a Codex custom plan no longer overwrites an existing Claude plan.

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ codeburn status # compact one-liner (today + month)
7878
codeburn status --format json
7979
codeburn export # CSV with today, 7 days, 30 days
8080
codeburn export -f json # JSON export
81-
codeburn share # redacted JSON bundle for support/debugging
81+
codeburn share # redacted JSON bundle; prompts omitted by default
8282
codeburn optimize # find waste, get copy-paste fixes
8383
codeburn optimize -p week # scope the scan to last 7 days
8484
codeburn compare # side-by-side model comparison
@@ -344,9 +344,10 @@ codeburn share # 7-day redacted JSON bundle
344344
codeburn share -p 30days # last 30 days
345345
codeburn share --provider claude # provider-specific bundle
346346
codeburn share --project api -o api-share.json
347+
codeburn share --include-prompts # opt in to redacted prompt text
347348
```
348349

349-
The bundle keeps enough structure to debug provider parsing and cost attribution: pseudonymous projects, sessions, turns, models, token usage, tools, activity categories, and costs. Redaction is best-effort; review the generated file before posting it publicly.
350+
The default bundle omits prompt text (`userMessage: null`) and keeps enough structure to debug provider parsing and cost attribution: pseudonymous projects, sessions, turns, models, token usage, tools, activity categories, and costs. `--include-prompts` keeps best-effort redacted prompt text for cases where maintainers explicitly need it. Review the generated file before posting it publicly.
350351

351352
## Menu Bar
352353

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,7 @@ program
706706
.option('--provider <provider>', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
707707
.option('--project <name>', 'Include only projects matching name (repeatable)', collect, [])
708708
.option('--exclude <name>', 'Exclude projects matching name (repeatable)', collect, [])
709+
.option('--include-prompts', 'Include redacted user prompts. By default prompts are omitted.')
709710
.action(async (opts) => {
710711
let customRange: DateRange | null = null
711712
try {
@@ -744,6 +745,7 @@ program
744745
provider: opts.provider,
745746
project: opts.project,
746747
exclude: opts.exclude,
748+
includePrompts: opts.includePrompts === true,
747749
})
748750
const savedPath = await writeRedactedShare(share, outputPath)
749751
console.log(`\n Redacted share exported to: ${savedPath}`)

src/share.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type RedactedShareOptions = {
1313
provider: string
1414
project: string[]
1515
exclude: string[]
16+
includePrompts?: boolean
1617
}
1718

1819
type RedactedCall = {
@@ -37,7 +38,7 @@ type RedactedTurn = {
3738
subCategory?: string
3839
retries: number
3940
hasEdits: boolean
40-
userMessage: string
41+
userMessage: string | null
4142
assistantCalls: RedactedCall[]
4243
}
4344

@@ -78,6 +79,7 @@ export type RedactedShare = {
7879
redaction: {
7980
applied: true
8081
placeholders: Record<RedactionKind, string>
82+
prompts: 'omitted' | 'redacted'
8183
uniqueReplacements: RedactionStats
8284
}
8385
summary: {
@@ -241,20 +243,20 @@ function redactCall(call: ParsedApiCall, redactor: StableRedactor): RedactedCall
241243
}
242244
}
243245

244-
function redactTurn(turn: ClassifiedTurn, redactor: StableRedactor): RedactedTurn {
246+
function redactTurn(turn: ClassifiedTurn, redactor: StableRedactor, includePrompts: boolean): RedactedTurn {
245247
return {
246248
timestamp: turn.timestamp,
247249
sessionId: redactor.redactText(turn.sessionId),
248250
category: turn.category,
249251
...(turn.subCategory ? { subCategory: redactor.redactText(turn.subCategory) } : {}),
250252
retries: turn.retries,
251253
hasEdits: turn.hasEdits,
252-
userMessage: redactor.redactText(turn.userMessage),
254+
userMessage: includePrompts && turn.userMessage ? redactor.redactText(turn.userMessage) : null,
253255
assistantCalls: turn.assistantCalls.map(call => redactCall(call, redactor)),
254256
}
255257
}
256258

257-
function redactSession(session: SessionSummary, redactor: StableRedactor): RedactedSession {
259+
function redactSession(session: SessionSummary, redactor: StableRedactor, includePrompts: boolean): RedactedSession {
258260
return {
259261
sessionId: redactor.redactText(session.sessionId),
260262
firstTimestamp: session.firstTimestamp,
@@ -265,12 +267,13 @@ function redactSession(session: SessionSummary, redactor: StableRedactor): Redac
265267
totalCacheReadTokens: session.totalCacheReadTokens,
266268
totalCacheWriteTokens: session.totalCacheWriteTokens,
267269
apiCalls: session.apiCalls,
268-
turns: session.turns.map(turn => redactTurn(turn, redactor)),
270+
turns: session.turns.map(turn => redactTurn(turn, redactor, includePrompts)),
269271
}
270272
}
271273

272274
export function buildRedactedShare(projects: ProjectSummary[], options: RedactedShareOptions): RedactedShare {
273275
const redactor = new StableRedactor()
276+
const includePrompts = options.includePrompts ?? false
274277
const sessions = projects.flatMap(project => project.sessions)
275278
const turns = sessions.flatMap(session => session.turns)
276279

@@ -283,7 +286,7 @@ export function buildRedactedShare(projects: ProjectSummary[], options: Redacted
283286
projectPath: redactor.redactText(project.projectPath),
284287
totalCostUSD: roundCost(project.totalCostUSD),
285288
totalApiCalls: project.totalApiCalls,
286-
sessions: project.sessions.map(session => redactSession(session, redactor)),
289+
sessions: project.sessions.map(session => redactSession(session, redactor, includePrompts)),
287290
}))
288291

289292
const uniqueReplacements = redactor.stats()
@@ -309,6 +312,7 @@ export function buildRedactedShare(projects: ProjectSummary[], options: Redacted
309312
project: '[project:<index>]',
310313
secret: '[secret:<index>]',
311314
},
315+
prompts: includePrompts ? 'redacted' : 'omitted',
312316
uniqueReplacements,
313317
},
314318
summary: {

tests/share.test.ts

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('redacted share', () => {
7777
}
7878
})
7979

80-
it('builds a useful redacted support bundle', () => {
80+
it('omits user prompts from support bundles by default', () => {
8181
const projects: ProjectSummary[] = [{
8282
project: 'client-a',
8383
projectPath: '/Users/husam/work/client-a',
@@ -147,8 +147,60 @@ describe('redacted share', () => {
147147
expect(share.projects[0]!.project).toBe('[project:1]')
148148
expect(share.projects[0]!.projectPath).toBe('[path:1]')
149149
expect(share.projects[0]!.sessions[0]!.totalCostUSD).toBe(1.2346)
150-
const message = share.projects[0]!.sessions[0]!.turns[0]!.userMessage
151150
const call = share.projects[0]!.sessions[0]!.turns[0]!.assistantCalls[0]!
151+
expect(share.redaction.prompts).toBe('omitted')
152+
expect(share.projects[0]!.sessions[0]!.turns[0]!.userMessage).toBeNull()
153+
expect(call.skills).toEqual(['browser-use'])
154+
expect(call.hasAgentSpawn).toBe(true)
155+
expect(call.hasPlanMode).toBe(true)
156+
})
157+
158+
it('can include redacted prompts when explicitly requested', () => {
159+
const projects: ProjectSummary[] = [{
160+
project: 'client-a',
161+
projectPath: '/Users/husam/work/client-a',
162+
totalCostUSD: 1.23456,
163+
totalApiCalls: 1,
164+
sessions: [{
165+
sessionId: 'session-1',
166+
project: 'client-a',
167+
firstTimestamp: '2026-05-05T10:00:00.000Z',
168+
lastTimestamp: '2026-05-05T10:01:00.000Z',
169+
totalCostUSD: 1.23456,
170+
totalInputTokens: 1000,
171+
totalOutputTokens: 200,
172+
totalCacheReadTokens: 50,
173+
totalCacheWriteTokens: 25,
174+
apiCalls: 1,
175+
turns: [{
176+
userMessage: 'fix client-a at /Users/husam/work/client-a for husam@example.com with token=secret-token-12345',
177+
assistantCalls: [],
178+
timestamp: '2026-05-05T10:00:00.000Z',
179+
sessionId: 'session-1',
180+
category: 'debugging',
181+
retries: 1,
182+
hasEdits: true,
183+
}],
184+
modelBreakdown: {},
185+
toolBreakdown: {},
186+
mcpBreakdown: {},
187+
bashBreakdown: {},
188+
categoryBreakdown: {},
189+
skillBreakdown: {},
190+
}],
191+
}]
192+
193+
const share = buildRedactedShare(projects, {
194+
label: '7 Days',
195+
range: { start: new Date('2026-05-01T00:00:00.000Z'), end: new Date('2026-05-07T23:59:59.999Z') },
196+
provider: 'all',
197+
project: [],
198+
exclude: [],
199+
includePrompts: true,
200+
})
201+
202+
const message = share.projects[0]!.sessions[0]!.turns[0]!.userMessage
203+
expect(share.redaction.prompts).toBe('redacted')
152204
expect(message).not.toContain('/Users/husam')
153205
expect(message).not.toContain('husam@example.com')
154206
expect(message).not.toContain('secret-token-12345')
@@ -157,9 +209,54 @@ describe('redacted share', () => {
157209
expect(message).toContain('[email:1]')
158210
expect(message).toContain('[project:1]')
159211
expect(message).toContain('[secret:1]')
160-
expect(call.skills).toEqual(['browser-use'])
161-
expect(call.hasAgentSpawn).toBe(true)
162-
expect(call.hasPlanMode).toBe(true)
212+
})
213+
214+
it('keeps null user prompts null even when prompt redaction is enabled', () => {
215+
const projects = [{
216+
project: 'client-a',
217+
projectPath: '/Users/husam/work/client-a',
218+
totalCostUSD: 0.01,
219+
totalApiCalls: 1,
220+
sessions: [{
221+
sessionId: 'session-1',
222+
project: 'client-a',
223+
firstTimestamp: '2026-05-05T10:00:00.000Z',
224+
lastTimestamp: '2026-05-05T10:01:00.000Z',
225+
totalCostUSD: 0.01,
226+
totalInputTokens: 10,
227+
totalOutputTokens: 5,
228+
totalCacheReadTokens: 0,
229+
totalCacheWriteTokens: 0,
230+
apiCalls: 1,
231+
turns: [{
232+
userMessage: null,
233+
assistantCalls: [],
234+
timestamp: '2026-05-05T10:00:00.000Z',
235+
sessionId: 'session-1',
236+
category: 'debugging',
237+
retries: 0,
238+
hasEdits: false,
239+
}],
240+
modelBreakdown: {},
241+
toolBreakdown: {},
242+
mcpBreakdown: {},
243+
bashBreakdown: {},
244+
categoryBreakdown: {},
245+
skillBreakdown: {},
246+
}],
247+
}] as unknown as ProjectSummary[]
248+
249+
const share = buildRedactedShare(projects, {
250+
label: '7 Days',
251+
range: { start: new Date('2026-05-01T00:00:00.000Z'), end: new Date('2026-05-07T23:59:59.999Z') },
252+
provider: 'all',
253+
project: [],
254+
exclude: [],
255+
includePrompts: true,
256+
})
257+
258+
expect(share.redaction.prompts).toBe('redacted')
259+
expect(share.projects[0]!.sessions[0]!.turns[0]!.userMessage).toBeNull()
163260
})
164261

165262
it('redacts project labels without mangling unrelated substrings', () => {
@@ -203,6 +300,7 @@ describe('redacted share', () => {
203300
provider: 'all',
204301
project: ['api'],
205302
exclude: ['client-a'],
303+
includePrompts: true,
206304
})
207305

208306
const message = share.projects[0]!.sessions[0]!.turns[0]!.userMessage
@@ -256,6 +354,7 @@ describe('redacted share', () => {
256354
provider: 'all',
257355
project: [],
258356
exclude: [],
357+
includePrompts: true,
259358
})
260359

261360
expect(share.projects[0]!.sessions[0]!.turns[0]!.userMessage).toBe('[project:1] uses token=[secret:1]')

0 commit comments

Comments
 (0)