# Proof audit pass 1 — fixture matrix

PR [#26066](https://git.cafe/pingdotgg/t3code/pull/26066), also mirrored as [#26066](https://github.com/pingdotgg/t3code/pull/26066).

Reviewed head: [ba74721bab](https://github.com/pingdotgg/t3code/commit/ba74721bab6a56721f03f45925090b0d2bffef5b), branch `fix/issue-26051`, against the supplied/PR-observed base [d0b81ceb6e](https://github.com/pingdotgg/t3code/commit/d0b81ceb6e225a7386203d590ec84def4abed281). Local `main` is older and is not the baseline for this matrix. This is a read-only, code-derived plan; no browser, server, provider turn, or test was run by this auditor. No capture exists in this pass. The 60-minute budget was not exhausted.

## Findings

1. **Evidence gate is open.** The current PR body has a Verification section and before/after runtime text, but no `## Evidence` section or linked captures. Its sentence “No render path changes, so there are no captures” does not satisfy the requested evidence gate. Show the actual No project chat tool execution and preserved sidebar membership. Stills alone cannot prove the turn sequence.
2. **Keep proof specific to fresh threads in workspace-write.** The change only allocates new Scratch folders. Existing threads retain their stored path; a failed pre-fix thread resumed after updating is not a valid after fixture. Full Access bypasses the intended sandbox test. Auto / `runtimeMode: auto` maps to Codex `workspaceWrite`; `auto-accept-edits` also uses it, but needs no separate capture.
3. **macOS remains an explicit coverage limit.** This host is Linux. A Linux bubblewrap reproduction plus the real-path assertion does not verify macOS seatbelt behavior. Label it `cannot-verify macos`; do not imply a native macOS pass.

No independent code-defect finding is claimed by this evidence-only pass. Test results and runtime text in the PR body were read as claims, not re-executed evidence.

## Minimum capture matrix

All Linux rows use **1440 × 1000 CSS pixels, dark theme, sidebar open, one connected environment, one fresh Scratch thread, no search, no unrelated panels**. Use the same Codex model, CLI version, sandbox settings, prompt, and viewport before/after. Theme and width do not control this server branch, so light/mobile-width permutations add no proof.

| ID / named fixture | Affected by diff? / data needed | Real-app seed and route | Expected visible sequence and assertions | Minimum artifact / status |
|---|---|---|---|---|
| SYM-B — Linux, symlinked Scratch, before | Yes, failing baseline. Supplied base revision; fresh home outside any git checkout; `home/scratch` symlink to an existing real directory outside that home; Codex Auto (`auto`, `workspaceWrite`); fresh thread. | Launch base app with explicit `--home-dir <fresh-before-home>`, no live userdata. In web new-task project picker choose **No project**, select Codex and Auto, submit the common prompt. Resulting chat route is `/<environmentId>/<threadId>`. | Record submit → actual shell tool call → bubblewrap failure containing “crosses writable symlink” / “cannot enforce sandbox read-only path”. Expand tool details if necessary. No marker file should exist. Show No project context. An auth/network/model-capacity failure is not this fixture. | **recording**, 1440×1000 × dark × SYM-B; pending, no capture. Optional final error still complements the video. |
| SYM-A — Linux, symlinked Scratch, after + No project identity | Yes, primary fix and grouping guard. Exact reviewed head; a separate fresh home with the same symlink topology and a fresh thread; same provider/mode/prompt. | Same app flow on reviewed head. Keep default sidebar; select its **No project** project scope after the turn and show the active thread. If using legacy sidebar instead, expand its **No project** group. Keep the chat breadcrumb visible. | Record submit → shell completion with exit 0, physical target path from `pwd`, and `ok`. Show thread still belongs to No project, with no new normal project created for the resolved folder. Pair with real-path/marker assertions below. Sidebar identity is bundled here, not a separate provider run. | **recording**, 1440×1000 × dark × SYM-A; pending, no capture. Optional final success still complements the video. |
| PLAIN-A — Linux, ordinary Scratch directory, after | Executed changed code; regression guard. Reviewed head; third fresh home with a real `scratch` directory, no symlink in any home ancestor; same Codex mode/prompt; new thread. | Same No project app flow; same chat route shape. Ordinary directory can be created by the app. | Record shell completion with exit 0 and `ok`, marker created under the normal scratch root, and thread still in No project. Confirms canonicalization did not break the ordinary configuration. | **recording**, 1440×1000 × dark × PLAIN-A; pending, no capture. A separate plain-before recording is unnecessary. |
| MAC-SYM — macOS, symlinked Scratch or symlinked home ancestor | Yes, same canonicalization, different native sandbox. Actual macOS server + Codex workspace-write required; fresh Scratch threads on base and head. A Linux browser emulating a Mac does not seed this. | On a separately authorized macOS environment, use the SYM-B / SYM-A app flow and topology. A naturally aliased `/var` → `/private/var` ancestor is relevant to path canonicalization. | Native before failure should identify a symlink component in the writable root; native after shell succeeds with physical cwd and preserved No project membership. Linux tests cannot establish this platform result. | **recording** if performed; **cannot-verify macos** on this VM, no capture. Explicit limitation; excluded from minimum Linux capture set. |

**Minimum set: three real-app recordings**: SYM-B, SYM-A, PLAIN-A, each at 1440×1000/dark. This is three viewport × theme × row combinations. Capture error/success endpoint stills as convenient PR previews, but they do not replace recordings. MAC-SYM is a declared platform gap, not a fabricated harness pass. Default and legacy sidebar, themes, and narrower widths are unchanged render implementations, so they do not multiply the provider recordings.

## Common seed / driver contract for the capturer

- Use a new timestamped run directory under the capturer's assigned scratch directory. Keep raw output, recordings, supporting assertions, and sanitized provenance there. Never delete the last run or use the lead-owned checkout as disposable state. Do not start a server against real `~/.t3/userdata` or use symlinked copies of live data.
- Fresh home must be outside a git checkout: Scratch is unavailable when the base directory is inside one. Create separate homes and targets for SYM-B and SYM-A so the after run cannot reuse the failed thread. A sibling real target outside the symlinked home is easy to distinguish in `pwd` output.
- Use the repo dev runner with an explicit `--home-dir` and isolated state; keep dev single-origin. Do not set `VITE_HTTP_URL` or `VITE_WS_URL`. Use existing authorized Codex authentication without copying credential directories or publishing tokens. Record server PIDs at spawn and stop only those PIDs at cleanup.
- Common prompt: **Run exactly `pwd && touch made-by-codex && echo ok` in your current working directory. Do not change directories, elevate permissions, disable the sandbox, or retry outside the sandbox. Report the exact output.** Show the actual tool result, not just the assistant's summary. A completed empty response is insufficient.
- Prefer the real UI for selecting No project and submitting. An existing app/RPC driver may call `WS_METHODS.projectsEnsureScratch`, then `ORCHESTRATION_V2_WS_METHODS.launchThread` with returned projectId, a fresh command/thread id, enabled Codex `modelSelection`, `runtimeMode: auto`, `interactionMode: default`, `workspaceStrategy: { type: root }`, and the prompt as `initialMessage` with empty attachments. Use the existing authenticated client schema; do not hand-edit the database or substitute a fake provider transcript. Navigate to `/<environmentId>/<threadId>` to record the real chat.
- With default sidebar, No project is a project scope/filter over a flat thread list. Showing a generic active row is insufficient membership proof: select that scope and show the thread still present, plus the No project breadcrumb. With `legacySidebarEnabled: true` (Settings, **Sidebar (legacy)**), show the thread below the expanded No project group. Either representation suffices; no renderer changed.
- Supporting sanitized manifest: exact app SHA; OS; Codex version and model; runtime mode and actual sandbox policy; symlink source and physical target; threadId/projectId; saved thread.worktreePath; saved project.workspaceRoot; advertised scratchWorkspaceRoot; actual tool exit/output; marker existence. On head assert `thread.worktreePath == realpath(thread.worktreePath)` and its parent is `realpath(home/scratch)`. Assert `project.workspaceRoot == scratchWorkspaceRoot == configured home/scratch`; thread.projectId must still be that Scratch project. On base the allocated string retains the symlinked prefix; tool output should show the actual sandbox error.
- Preserve comparable prompt, settings, and topology at both revisions. The PR's existing transcript names another pre-fix main revision; a fresh capture should use the supplied base or explicitly record and justify a different unmodified pre-fix SHA.
- Capture can run on a browser connected locally, remotely, or through the normal app transport. The filesystem and Codex process are on the server host; changing browser platform is not changing sandbox platform.
- Any actual browser session belongs to the lead/capturer's authorized integrated verification, not this pass. If the real app cannot reach a row, mark it blocked. A service/mock screenshot must be explicitly labelled **harness capture**, and cannot satisfy SYM-B/SYM-A/PLAIN-A's real Codex requirement.

## Code-derived caller and branch inventory

There is **no changed render component or changed prop contract**. Production [ManagedProjectFolders.folderForThread](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/apps/server/src/project/ManagedProjectFolders.ts#L317) has one caller, [ThreadLaunchService.launch](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/apps/server/src/orchestration-v2/ThreadLaunchService.ts#L871). Its existing direct service tests are the only other invocations.

| Branch / data / interaction | Classification and proof treatment |
|---|---|
| Scratch unavailable (home inside checkout or probe failure) | `folderForThread` returns None before the new realPath call; UI Scratch availability still follows connected environment + advertised scratch root. Unchanged, no capture row. |
| Non-Scratch project | Returns None before changed code. Named projects, ordinary repositories, and explicit existing_worktree paths do not exercise this allocation. Unchanged control, existing service test inspected; no new visual row. |
| New root-strategy Scratch launch; requested worktree normalized to root | New realPath call runs; returned folder becomes existing_worktree strategy and persisted thread.worktreePath. Covered by SYM-A / PLAIN-A. |
| First create, empty/draft promotion, first message, title fallback | Allocation text is `initialMessage.text ?? title`. New normal/draft launches converge on the same caller. Empty threads can allocate a folder but do not test sandbox execution; capture the first submitted shell turn. |
| Accepted launch receipt/retry; pre-existing thread resumed; bound scheduled run | Existing persisted path is reused; no allocation is rerun. Pre-fix symlinked threads are not migrated. This known limitation is in the PR body; exclude from after success claims. Fresh unbound scheduled launches still use allocation. |
| Folder missing | Existing recursive mkdir occurs before realPath, so a missing ordinary Scratch folder is recreated. Existing focused test inspected; backend guard, no extra visual state. |
| Scratch directory itself is symlinked, or an ancestor is symlinked | Same physical-path resolution branch. SYM-A captures explicit scratch symlink. Ancestor symlinks are the same path equivalence class; symlinked TMPDIR test claim is backend support, not native macOS proof. |
| realPath fails after mkdir, or mkdir/claim fails | ScratchFolderError maps through launch's `provision-worktree` failure. No provider turn starts. Deterministic realPath-only failure would need a FileSystem fault-injection harness; no new bespoke UI error layout is introduced. Any capture of this branch is a labelled **harness capture**, and would be **recording**; not required in the visual minimum. New realPath error handling is inspected only, not fault-injected in this pass. |
| Short-id collision → full-id collision → random suffix | Existing claimFreeFolder loop is unchanged; all candidates now join to realRoot. Existing test exercises all three names and existence/uniqueness. No visually distinct layout or extra capture. |
| Message empty/non-ASCII, long text, punctuation/traversal-looking text; date; UUID | Existing folderWords uses at most five words/48 characters; basename includes date and sanitized id. Only parent path changes. Existing long-text and collision assertions inspected. Do not create a length/count screenshot grid. |
| Fresh → running → completed/error; expanding tool details; selecting No project | Observable lifecycle/interaction is part of each required recording; stills alone are insufficient. Date in a folder name does not require a separate timed capture. |

### Entry points and surfaces traced

- [WebSocket launch and ensure-Scratch handlers](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/apps/server/src/ws.ts#L2067), shared [ThreadMessageIntake.launchThread](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/apps/server/src/orchestration-v2/ThreadMessageIntake.ts#L209), MCP [project launch handler](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/apps/server/src/mcp/toolkits/project/handlers.ts#L60), and [fresh scheduled-task launches](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/apps/server/src/scheduledTasks/ScheduledTaskService.ts#L813) converge on ThreadLaunchService. ACP import in ws.ts also calls launch with root strategy; it does not need a Codex evidence fixture.
- Web Scratch creation entry points: `useScratchProject` actions, NoProjectsHero, CommandPalette, chatRouteCommands keybinding, DraftHeroHeadline project selection, useDraftEnvironmentRouting, NewThreadLinkView (also used by prompt-link landing), and desktop DesktopWebLinkCoordinator. They resolve the same Scratch project; no new Settings toggle starts a different allocation path. Settings scheduled tasks reach the shared scheduler path. Mobile NewTaskRouteScreen / NewTaskDraftScreen also create or select Scratch through shared project commands.
- [RuntimePolicy](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/apps/server/src/orchestration-v2/RuntimePolicy.ts#L97) selects thread.worktreePath before project.workspaceRoot, and [Codex runtime-mode mapping](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts#L687) sets workspaceWrite for Auto. Claude, Cursor, Grok, OpenCode, and Antigravity receive the same canonical cwd for fresh Scratch launches; no adapter code changed. They are not independent reproductions of the Codex sandbox bug. Read-only / Full Access modes do not establish the requested writable-sandbox fix.
- [ChatHeader](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/apps/web/src/components/chat/ChatHeader.tsx#L81) uses project.isScratch for its No project label. [isScratchProject](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/packages/client-runtime/src/state/projects.ts#L138) compares project.workspaceRoot with advertised scratchWorkspaceRoot. [Project grouping](https://github.com/pingdotgg/t3code/blob/ba74721bab6a56721f03f45925090b0d2bffef5b/packages/client-runtime/src/state/projectGrouping.ts#L350) uses project identity/isScratch, not thread.worktreePath. Canonicalization must not rewrite either project-level path.
- Web chat route `/<environmentId>/<threadId>` is hosted by the `_chat` layout; desktop wraps that web UI. AppSidebarLayout selects Sidebar or LegacySidebar by the existing legacySidebarEnabled setting. SidebarThreadRow/ChatView workspace and git operations consume worktreePath as a cwd; its nullability and shape do not change, only the string's physical parent. Branch nullability, title lengths, loading/empty/search/error list states, expanded/collapsed sidebar, counts, remote machine labels, themes, narrow layout, web vs Electron chrome, and mobile native navigation are unchanged layout branches. Only actual tool success/error output and path text differ, covered by the three recordings. No client or transport schema changed; no separate local/relay/tunnel screenshot grid is justified.

## Handoff acceptance

Add a `## Evidence` section to the PR in the lead's write-authorized phase. Label Before / After, embed preview stills if useful, link/embed the three recordings and sanitized manifest, record exact revisions, and keep `cannot-verify macos` and existing-thread limitation explicit. This auditor does not edit the PR or post a comment.

FRICTION: none
