diff --git a/packages/paperclip-runner/devtools/issue-thread/src/ThreadItems.tsx b/packages/paperclip-runner/devtools/issue-thread/src/ThreadItems.tsx
index 2dafedf6b7e..9f6b1059837 100644
--- a/packages/paperclip-runner/devtools/issue-thread/src/ThreadItems.tsx
+++ b/packages/paperclip-runner/devtools/issue-thread/src/ThreadItems.tsx
@@ -88,7 +88,7 @@ function ProgressItem({
{item.eventCount} sanitized progress update{item.eventCount === 1 ? "" : "s"} observed.
- Raw chain-of-thought and provider text are not exposed.
+ Raw output, chain-of-thought, and credential values are not exposed.
diff --git a/packages/paperclip-runner/docs/capability-clean-room-chat.md b/packages/paperclip-runner/docs/capability-clean-room-chat.md
index 981cdfd49ce..f986acf9e3e 100644
--- a/packages/paperclip-runner/docs/capability-clean-room-chat.md
+++ b/packages/paperclip-runner/docs/capability-clean-room-chat.md
@@ -130,8 +130,9 @@ While a turn runs, a live status rail appears immediately after send and tracks
the newest safe Codex activity. Reasoning, planning, shell-command, file-change,
MCP/dynamic-tool, assistant-stream, and Paperclip semantic-tool lifecycle events
each trigger an interim frame. Discrete tools remain separate rows; only noisy
-text deltas are grouped. Raw provider text, command output, and chain of thought
-stay withheld.
+text deltas are grouped. Structured shell items show a bounded,
+credential-redacted command preview; raw command output, other provider
+payloads, and chain of thought stay withheld.
## Remote preview gateway
diff --git a/packages/paperclip-runner/src/issue-thread/live-projection.ts b/packages/paperclip-runner/src/issue-thread/live-projection.ts
index 75320734769..497068b5a06 100644
--- a/packages/paperclip-runner/src/issue-thread/live-projection.ts
+++ b/packages/paperclip-runner/src/issue-thread/live-projection.ts
@@ -772,6 +772,7 @@ export function projectCapabilityIssueThread(
event: keyof typeof PROGRESS_EVENTS;
at: string;
count: number;
+ command: string;
}
>();
for (const entry of snapshot.evidence) {
@@ -784,7 +785,7 @@ export function projectCapabilityIssueThread(
const key = event.endsWith("_delta") ? `${entry.turnId}:${event}` : entry.id;
const group = progressGroups.get(key);
if (group === undefined) {
- progressGroups.set(key, { id: entry.id, turnId: entry.turnId, event, at: entry.at, count: 1 });
+ progressGroups.set(key, { id: entry.id, turnId: entry.turnId, event, at: entry.at, count: 1, command: readString(entry.data.command) });
} else {
group.count += 1;
}
@@ -799,7 +800,7 @@ export function projectCapabilityIssueThread(
activity: copy.activity,
status: running ? "running" : "complete",
label: copy.label,
- summary: running ? copy.running : copy.complete,
+ summary: group.command || (running ? copy.running : copy.complete),
eventCount: group.count,
});
}
diff --git a/packages/paperclip-runner/src/issue-thread/public-view.test.ts b/packages/paperclip-runner/src/issue-thread/public-view.test.ts
index 3683fc7a095..f5488eb8e73 100644
--- a/packages/paperclip-runner/src/issue-thread/public-view.test.ts
+++ b/packages/paperclip-runner/src/issue-thread/public-view.test.ts
@@ -219,6 +219,27 @@ describe("Capability evidence redaction", () => {
}
});
+ it("shows bounded structured shell commands while redacting credential values", () => {
+ const redacted = redactCapabilityEvidenceData("provider_event", {
+ method: "item/started",
+ params: {
+ item: {
+ type: "commandExecution",
+ source: "unifiedExecStartup",
+ command: "/bin/bash -lc 'curl -H \"Authorization: Bearer hidden-token\" --token pcp_1234567890 https://example.test && printf ok'",
+ aggregatedOutput: CANARY,
+ },
+ },
+ });
+
+ expect(redacted.event).toBe("command_started");
+ expect(redacted.command).toContain("/bin/bash -lc");
+ expect(redacted.command).toContain("printf ok");
+ expect(redacted.command).not.toContain("hidden-token");
+ expect(redacted.command).not.toContain("pcp_1234567890");
+ expect(JSON.stringify(redacted)).not.toContain(CANARY);
+ });
+
it("collapses an unknown provider method rather than echoing it", () => {
expect(redactCapabilityEvidenceData("provider_event", { method: `x/${CANARY}` })).toEqual({
event: "other",
diff --git a/packages/paperclip-runner/src/live/evidence-redaction.ts b/packages/paperclip-runner/src/live/evidence-redaction.ts
index 733d9aadd5c..315574bebe1 100644
--- a/packages/paperclip-runner/src/live/evidence-redaction.ts
+++ b/packages/paperclip-runner/src/live/evidence-redaction.ts
@@ -41,6 +41,7 @@ export type CapabilityEvidenceKind =
/** Disposition copy is rendered, so it is bounded rather than unbounded. */
const MAX_DISPOSITION_CHARS = 500;
+const MAX_COMMAND_CHARS = 2_000;
const MAX_FIELD_NAMES = 32;
const MAX_ENTITY_REFS = 64;
@@ -130,6 +131,21 @@ function clamp(value: string, limit: number): string {
return value.length <= limit ? value : `${value.slice(0, limit)}…`;
}
+function commandPreview(item: Record): string | null {
+ // `unifiedExecStartup` is the Codex app-server's structured shell item. Do
+ // not start echoing arbitrary future provider item shapes just because they
+ // happen to grow a field named `command`.
+ if (asString(item.source) !== "unifiedExecStartup") return null;
+ let command = clamp(asString(item.command), MAX_COMMAND_CHARS);
+ if (command.length === 0) return null;
+ command = command
+ .replace(/bearer\s+[a-z0-9._-]+/gi, "Bearer [redacted]")
+ .replace(/\b(?:sk|pcp)_[a-z0-9._-]{8,}\b/gi, "[redacted]")
+ .replace(/((?:api[_-]?key|access[_-]?token|auth[_-]?token|password|secret)\s*=\s*)(?:"[^"]*"|'[^']*'|[^\s]+)/gi, "$1[redacted]")
+ .replace(/(--(?:api-key|token|password|secret)\s+)(?:"[^"]*"|'[^']*'|[^\s]+)/gi, "$1[redacted]");
+ return command;
+}
+
function copyFields(
data: Record,
names: readonly string[],
@@ -210,10 +226,17 @@ export function redactCapabilityEvidenceData(
data: Record,
): Record {
switch (kind) {
- case "provider_event":
+ case "provider_event": {
+ const event = capabilityProviderEventCategory(asString(data.method), asRecord(data.params));
+ const item = asRecord(asRecord(data.params).item);
+ const command = event === "command_started" || event === "command_completed"
+ ? commandPreview(item)
+ : null;
return {
- event: capabilityProviderEventCategory(asString(data.method), asRecord(data.params)),
+ event,
+ ...(command === null ? {} : { command }),
};
+ }
case "diagnostic":
// A provider diagnostic is operator surface, not board surface. Only the
// fact that one was recorded survives.
@@ -343,6 +366,7 @@ export function capabilityEvidenceDetails(
switch (kind) {
case "provider_event":
add("Event", data.event);
+ add("Command", data.command);
break;
case "diagnostic":
add("Visibility", data.diagnostic);