new file mode 100644
index 0000000..7e3fa19
@@ -0,0 +1,72 @@
+---
+visibility: public
+---
+
+# Spec 004 — File browser
+
+**Status:** Draft
+**Related:** [PRD FR-19](../prd/001-picortex-v1.md#mobile-first-web-ui), [bettergpt mockups](../mockups/README.md)
+
+## Goal
+
+For any chat, show the chat's home directory as a tree. Tapping a file shows its contents (syntax-highlighted for code, rendered for markdown, image-preview for images). Inspiration: Cortex's `mockups/bettergpt/index-split-view-filebrowser.html`.
+
+## Routes
+
+- `GET /api/chats/:chat_id/files/tree?path=<subpath>` — JSON: `[{name, path, type:"dir"|"file", size, mtime}]`
+- `GET /api/chats/:chat_id/files/content?path=<subpath>` — raw bytes; client picks renderer from MIME/extension
+- `POST /api/chats/:chat_id/files?path=<subpath>` — write (later)
+- `DELETE /api/chats/:chat_id/files?path=<subpath>` — delete (later)
+
+Backend runs each as `sudo -u chat-<hex>` → reads the chat user's home. Paths are always relative to home; `..` is rejected via realpath-and-prefix check.
+
+## UI
+
+### Desktop / tablet
+
+Three columns: `[Messages | File tree | File viewer or Terminal]`. Tree + viewer share ~50/50 of the right two-thirds; messages fixed 1/3.
+
+### Mobile
+
+Swipe-tab layout. Tab 2 is the file browser; it splits vertically into tree (top 40%) + viewer (bottom 60%) once a file is selected. Back-gesture returns to messages.
+
+Mockup references in [docs/mockups/README.md](../mockups/README.md).
+
+## Renderers
+
+| Type | Renderer |
+|---|---|
+| Markdown | `react-markdown` with `remark-gfm` |
+| Code | `react-syntax-highlighter` with Prism themes |
+| JSON/YAML | syntax-highlighted, collapsible tree |
+| Image | `<img>` with width 100% |
+| PDF | `<iframe>` or `react-pdf` (post-v1) |
+| Binary | "N bytes, not previewed" + download link |
+
+## Hidden files
+
+Show dotfiles by default (Jacob wants to see `.picortex/prompts/discriminator.md`). Exclude `.git/objects/*` (noise) behind a "show all" toggle.
+
+## Security
+
+- Path check: `realpath(home + path)` must start with `realpath(home)`.
+- Enforce read-size cap (10 MB). Above that, return `413`.
+- Content-type sniff server-side; never echo untrusted `Content-Type` headers from the filesystem.
+- HTML content is returned as `text/plain` unless explicitly requested as rendered markdown.
+
+## Caching
+
+- Tree responses cached with `ETag` (home-dir mtime + size-of-listing hash)
+- File content cached with `ETag` (file mtime + size)
+- Browser cache: `Cache-Control: private, max-age=5`
+
+## Testing
+
+- **Unit:** path-sanitizer rejects `..`, symlinks-out, absolute paths, null bytes.
+- **Integration:** real filesystem; navigate, assert sorted, assert rejected on another chat's home.
+- **E2E:** Playwright on mobile; tap through tree, render markdown.
+
+## Open questions
+
+- OQ1: Do we want write support in v1? (Yes, for editing `.picortex/prompts/discriminator.md` directly.) Post-S7.
+- OQ2: Search within a chat's files? (Defer to v0.2 — the terminal can `rg`.)
\ No newline at end of file