prose
CLAUDE.md8 starsChanged 5 months agoMITView on GitHub
OpenAI Codex and other non-Claude agents use AGENTS.md as their repository entry point. Claude agents should also read AGENTS.md for shared cross-agent coordination rules, then use this file for Claude-specific workflows, skills, and deeper project context. Before any planning, prioritization, sequencing, or parallelization work — including "what should we work on / what's next," scoping a wave or epic, or proposing how to split work across agents — read docs/roadmap.md first. It is the canonical, resumable source of truth for…
- Reads credentials
In this repository31 other files it gives its agents.
- AGENTS.mdAGENTS.md
- accelerateSkill
- implement-issueSkill
- pipeline-engSkill
- pipeline-orchestrateSkill
- pipeline-peSkill
- pipeline-scorerSkill
- qa-acceleratedSkill
- qa-prSkill
- qa-webSkill
- releaseSkill
- review-feedbackSkill
- roadmap-prioritizationSkill
- roadmap-refinementSkill
- security-auditSkill
- accelerateSkill
- implement-issueSkill
- pipeline-engSkill
- pipeline-orchestrateSkill
- pipeline-peSkill
- pipeline-scorerSkill
- qa-acceleratedSkill
- qa-hitlSkill
- qa-prSkill
- qa-webSkill
- releaseSkill
- review-feedbackSkill
- roadmap-prioritizationSkill
- roadmap-refinementSkill
- screenshotsSkill
- security-auditSkill
The file
Show its rules and text
We split this CLAUDE.md into the rules it gives an agent
33 rules. About 0 are common, 17 are distinctive, and 16 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈17
Distinctive · estimate
This author's own choices. Worth a look.
16
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 19
Worktree git commands: Always use git -C <worktree-path> <subcommand> instead of cd <path> && git <subcommand>.
- Distinctive: line 38
Note: Always run these commands for the user rather than asking them to run manually.
- Distinctive: line 44
Before writing any code, complete this checklist:
- Very specific: line 46
Roadmap: Read docs/roadmap.md — confirm where the issue sits (wave / track) and how it sequences against other work.
- Very specific: line 47
Branch: Create or switch to issue branch: git checkout -b issue-<number>-<description>
- Distinctive: line 49
Verify: Run git branch --show-current to confirm you're not on main
- Distinctive: line 53
Before presenting completed work for user review, ensure a clean environment:
- Distinctive: line 55
Kill this project's dev server: Use PID file for safe cleanup (see below)
- Very specific: line 56
Start fresh dev server: Run npm run dev in the background
- Distinctive: line 57
Verify no errors: Check dev server output for compilation errors before announcing completion
- Distinctive: line 67
Before announcing a task complete, run git status.
- Distinctive: line 76
If a file's destination needs user input, ask explicitly — don't defer the decision by leaving it untracked.
- Very specific: line 86
Build first: Run npm run build to create fresh out/ files
- Distinctive: line 87
Launch with Circuit Electron: Use development mode to launch from project directory
- Very specific: line 89
Clean up screenshots: Delete electron-screenshot-*.jpeg files from the project root after testing
- Distinctive: line 113
If the PID file is missing but Electron processes exist, they're likely stale from a crashed session.
- Distinctive: line 119
Port conflicts: Default ports (5173, 9222) may already be in use
- Very specific: line 140
After npm run build:mac, the app is at:
- Distinctive: line 148
Track work in GitHub Issues: Create issues before starting work.
- Very specific: line 149
Branching: Create a branch per issue using format issue-<number>-<short-description> (e.g., issue-42-fix-login-bug).
- Very specific: line 152
Before merging any PR: Check for unresolved review comments (/review-feedback <pr-number> or gh api …
- Very specific: line 153
Always output links: After creating, closing, or commenting on issues or PRs, always output the full URL (e.g., …
- Very specific: line 171
Manual trigger: Run workflow_dispatch on review-feedback.yml with a PR number to re-analyze any PR.
- Very specific: line 177
All workflows in .github/workflows/:
- Very specific: line 200
Always use getApi() from src/renderer/lib/browserApi.ts instead of accessing window.api directly.
- Distinctive: line 204
When running as Electron:
- Very specific: line 269
Always bump
DB_VERSION: when adding/removing object stores. - Distinctive: line 270
Upgrades can be blocked: if the database is open in another tab.
- Very specific: line 292
Design before touching shared editor state: Only session.ts calls view.updateState, creates an EditorState or persists records.
- Very specific: line 300
Anthropic is the only provider: All legacy multi-provider code (OpenAI, OpenRouter, Ollama) has been removed.
- Distinctive: line 420
Component library: Use shadcn/ui components. Don't introduce other UI libraries.
- Distinctive: line 421
Animations: Keep minimal. Use only what shadcn/ui and tailwindcss-animate provide out of the box.
- Distinctive: line 426
When the user signals they want to stop working (e.g., "let's take a break", "let's stop for now", "I'm done for now"), automatically …
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
OpenAI Codex and other non-Claude agents use AGENTS.md as their repository entry point.
Claude agents should also read AGENTS.md for shared cross-agent coordination rules, then use
this file for Claude-specific workflows, skills, and deeper project context.
Roadmap & Planning — read docs/roadmap.md first
Before any planning, prioritization, sequencing, or parallelization work — including "what should we work on / what's next," scoping a wave or epic, or proposing how to split work across agents — read docs/roadmap.md first. It is the canonical, resumable source of truth for what we're building, in what order (the wave model), and how the work is parallelized (the three concurrent Wave 1 tracks and the A→B→C merge ladder). The sequencing and parallelization strategy already lives there — build on it, don't reinvent it. The project board (#5) is the live queue; the roadmap is the narrative + operating model (column = phase, milestone = wave). It changes frequently, so read the current version: if your branch may be behind main, run git fetch origin and read git show origin/main:docs/roadmap.md (or sync first) — a stale worktree can show an outdated or even retired roadmap.
Development Environment
This project uses an agentic, multi-agent development workflow:
- Multiple Claude Code agents may run simultaneously on the same machine — across terminal sessions, git worktrees, different branches, or different repos. Never assume you're the only agent running.
- Prefer Agent Teams and git worktrees for parallelizing independent work. Use
isolation: "worktree"when spawning agents that need to make changes without conflicting with the main working tree. This avoids branch conflicts and lets multiple agents write code simultaneously. Branch new worktrees off freshly-fetchedorigin/main(git fetch originfirst) — a worktree cut from a stale base shows outdated docs, including an oldroadmap.md. - Worktree git commands: Always use
git -C <worktree-path> <subcommand>instead ofcd <path> && git <subcommand>. Single commands match theBash(git:*)allowlist; compoundcd && gitchains get flagged for manual approval, which blocks unattended agents. - Cloud agents (via GitHub Actions) handle PR reviews, automated fixes, and pipeline triage. Local agents handle implementation, QA, and complex features.
- The CI pipeline is self-enhancing — automated review (
claude.yml), scoring (pipeline-triage.yml), and auto-fix (ci-gate.yml) run on every PR. Skills and workflows evolve alongside the codebase. - Be mindful of concurrent sessions. Other terminal sessions or worktree agents may be running builds, dev servers, or tests at the same time. Check for port conflicts before starting servers. Use PID files (not
pkillpatterns) for process management. UseghCLI (not GitHub MCP) for all GitHub operations. Alwaysgit fetch originbefore comparing branches — another session may have pushed. - The project board (GitHub Projects #5) tracks priority and in-progress work. Board #5 has no "In Progress" column — status columns are: User Requested, Do First, Do Next, Later, Quick Wins, On Hold. Never mutate board field definitions — only move items between existing columns. The overarching roadmap narrative (epics, waves, sequencing) lives in
docs/roadmap.md.
Commands
npm run dev # Start development (Electron + Vite HMR, debuggable on port 9222)
npm run build # Build for production (unpacked)
npm run build:mac # Build macOS distributable (.app + .dmg)
npm run build:mas # Build Mac App Store package (MAS_BUILD=1)
npm run build:win # Build Windows distributable
npm run build:linux # Build Linux distributableInstall: npm ci is enough. Electron ≥ 42 downloads nothing in its own postinstall; ours runs install-electron after electron-builder install-app-deps, so the binary lands at install time (electron-vite needs it before npm run dev). If a checkout ever lacks it, run npx install-electron.
Note: Always run these commands for the user rather than asking them to run manually. Start dev servers in the background so work can continue.
Testing: Unit tests via vitest (npm run test:unit; src/**/*.test.ts, jsdom, a headless TipTap editor built from createEditorExtensions(); runs in CI before the Electron suite). E2E tests via Playwright (e2e/, playwright.config.ts). Manual QA via Circuit Electron (see below).
Before Implementation
Before writing any code, complete this checklist:
- Roadmap: Read
docs/roadmap.md— confirm where the issue sits (wave / track) and how it sequences against other work. (Mandatory for any planning/sequencing/parallelization task.) - Branch: Create or switch to issue branch:
git checkout -b issue-<number>-<description> - Docs: For complex issues, create
docs/issues/<number>/plan.md - Verify: Run
git branch --show-currentto confirm you're not onmain
Before Presenting Work for Review
Before presenting completed work for user review, ensure a clean environment:
- Kill this project's dev server: Use PID file for safe cleanup (see below)
- Start fresh dev server: Run
npm run devin the background - Verify no errors: Check dev server output for compilation errors before announcing completion
This prevents LevelDB lock conflicts and ensures the user sees the latest changes.
Verification Instructions
Always include human-readable verification steps when presenting completed work. These should let the user independently confirm the changes work. Format as numbered steps with exact commands to run and what to look for in the output. Prefer verification against the locally running dev server; only use npm run build:mac + the built app when the change specifically requires a production build to verify.
Working Tree Hygiene
Before announcing a task complete, run git status. Every untracked file must have a destination — do not leave files orphaned across sessions.
- Project code/config (e.g.,
.claude/— standard Claude Code project config) → committed - Project docs → commit under
docs/(ordocs/issues/<n>/for issue-linked docs) - Historical/reference docs (launch checklists, audits, proposals) → commit under an appropriate
docs/subdirectory such asdocs/launch/ordocs/audits/ - Claude Cowork artifacts (
.skills/,*.plugin) → gitignored; these are per-session tooling outputs, not project files - Editor/IDE state (
.vscode/,.idea/,.zed/) → gitignored - Local-only scratch (one-off drafts) → either delete, or place under a gitignored path
If a file's destination needs user input, ask explicitly — don't defer the decision by leaving it untracked.
QA Testing
The app supports automated QA testing via Circuit Electron MCP (configured at parent level).
Workflow
Circuit Electron launches its own Electron instance, which bypasses electron-vite's dev server. For reliable testing:
- Build first: Run
npm run buildto create freshout/files - Launch with Circuit Electron: Use development mode to launch from project directory
- Test the feature: Interact with the app using Circuit Electron tools
- Clean up screenshots: Delete
electron-screenshot-*.jpegfiles from the project root after testing
Dev Server PID Protocol
The dev server writes its PID to .dev.pid on startup. Always use this file for process management—never use pattern matching like pgrep -f "prose.*Electron" which may match stale instances from other sessions.
# Check if running (use SEPARATE Bash calls — never chain with && or ||)
cat .dev.pid 2>/dev/null # Call 1: read the PID
ps -p <PID> -o pid= # Call 2: check if process exists
# Kill only THIS project's dev server
kill <PID> # Use the PID from call 1
# Fallback ONLY if PID file missing (use sparingly - may affect other agents)
pkill -f "prose.*Electron"
pkill -f "electron-vite.*prose"
# NEVER: pkill -f node # This kills Circuit Electron MCP!
# NEVER: pkill -f Electron # This kills ALL Electron apps!
# To clear LEAKED Circuit servers safely, use the reaper (orphans only) — see below:
# node scripts/reap-circuit.mjsIf the PID file is missing but Electron processes exist, they're likely stale from a crashed session. Kill them before starting fresh.
Multi-Agent Awareness
Multiple Claude Code agents may run simultaneously on this machine, potentially working on different Electron apps with similar signatures. This causes:
- Port conflicts: Default ports (5173, 9222) may already be in use
- Process confusion: Broad
pkillpatterns kill another agent's processes - Session conflicts: Circuit Electron sessions may collide
Always prefer the PID file method for cleanup. When encountering "address already in use" errors, check if another agent is active before assuming a bug. Vite automatically finds alternative ports, but DevTools debugging port (9222) conflicts will cause launch failures.
Orphaned Circuit Server Cleanup
Circuit Electron (@snowfort/circuit-electron, a 0.0.x alpha) does not exit when its Claude client disconnects. Orphaned servers reparent to launchd and many spin into ~100% CPU busy-loops; left alone they pile up (one sweep found 74 of them eating ~10 cores). The dev-server PID protocol above does not cover this — it kills the app, not the MCP server.
Reap them safely with:
node scripts/reap-circuit.mjs # kill orphaned servers
node scripts/reap-circuit.mjs --dry-run # report only, kill nothingThis is the safe inverse of "NEVER pkill -f node": it kills a Circuit process only if its parent chain reaches init (pid 1) through Circuit processes alone. Any server still attached to a live client — this session's, a concurrent agent's, even a terminal you launched it from — is spared, so it's multi-agent safe by construction. The QA skills (qa-pr, qa-accelerated, qa-hitl) invoke it automatically; run it by hand any time CPU spikes from stray node processes.
Built App Location
After npm run build:mac, the app is at:
dist/mac-arm64/prose.appWorkflow
- Use
ghCLI for all GitHub operations: Issues, PRs, project board, checks, releases — always useghcommands via Bash, never a GitHub MCP server. TheghCLI is pre-authenticated and available both locally and in CI. - Track work in GitHub Issues: Create issues before starting work.
- Branching: Create a branch per issue using format
issue-<number>-<short-description>(e.g.,issue-42-fix-login-bug). Branch frommain. - Commits: Reference issue numbers in commits (e.g.,
Fix login validation (#42)). - Pull requests: One PR per issue. Merge to
mainafter code review. - Before merging any PR: Check for unresolved review comments (
/review-feedback <pr-number>orgh api repos/solo-ist/prose/pulls/<number>/comments). Do not merge until all feedback is addressed, dismissed with rationale, or deferred to a follow-up issue. - Always output links: After creating, closing, or commenting on issues or PRs, always output the full URL (e.g.,
https://github.com/solo-ist/prose/pull/42) so the user can Cmd+click from the terminal. - Issue documentation: For complex issues, create a folder in
docs/issues/<number>/. Seedocs/issues/README.mdfor details.
Automated Review Analysis
Claude reviews a PR whenever E2E passes on it — on PR open and after every push — because ci-gate.yml's trigger-review job auto-posts a /review on green CI (self-debounced ~10 min). claude.yml has no pull_request trigger: a /review comment is the only way in. A second workflow (review-feedback.yml) auto-analyzes that review:
- Detection: Triggers on
issue_commentfromclaude[bot]containing## Code Review - Analysis: Calls Claude Sonnet API to categorize feedback (Blocking / Functional / Quality / Nitpicks / Questions)
- Output: Posts structured triage comment with severity, effort, and MERGE / FIX REQUIRED / NEEDS DISCUSSION recommendation
- Loop prevention: Analysis comments include a
<!-- review-feedback-analysis -->sentinel excluded from detection
On-demand review: Comment /review on a PR to trigger a fresh code review. You rarely need to — ci-gate.yml auto-posts /review whenever E2E passes (on PR open and after each push), so a green CI already triggers the review. A redundant manual /review double-runs the cloud reviewer and the review-feedback analysis — wasted cloud-agent runs.
When no auto-review appears on green CI, it was almost certainly the author-trust gate: trigger-review skips the review unless the PR author has repo write/admin (checked via getCollaboratorPermissionLevel — not author_association, which misreports private org members as CONTRIBUTOR). The gate lives in ci-gate.yml rather than claude.yml because the /review is posted as the PROJECT_TOKEN owner, who is always trusted — so claude.yml's commenter check structurally cannot catch a fork author, and an outside contributor could otherwise spend Anthropic credits on demand.
So a fork PR from an outside contributor goes green with no review, logging Notice — auto-review skipped (untrusted author) in the CI Gate run. To review it, post /review yourself: claude.yml checks the commenter's permission, so a maintainer comment passes, and the review reads the diff via gh pr diff, which works across forks. Same remedy when E2E was skipped entirely.
Manual trigger: Run workflow_dispatch on review-feedback.yml with a PR number to re-analyze any PR.
Local deep-dive: For code-level validation of review concerns, use /review-feedback <pr-number> locally. The cloud version is a quick triage; the local skill reads actual source files.
CI/CD Workflows
All workflows in .github/workflows/:
claude.yml- Code review +@claudemention handler; entered via/reviewcomment only (nopull_requesttrigger)review-feedback.yml- Analyzes claude[bot] review comments, posts structured triagee2e.yml- Electron Playwright tests on every PR (or/testcomment)web-e2e.yml- Browser Playwright tests foraccelerated-labeled or bot PRsgateway.yml- Gateway share integration suite (npm run test:shareagainst a Postgres service) on PRs touchinggateway/**share-viewer-e2e.yml- Shared web viewer suite (e2e/web.share-viewer.spec.ts) on PRs touching the viewer, the artifact builder, or the specci-gate.yml- On E2Eworkflow_runsuccess, auto-posts/review(the green-CI review trigger), gated on the PR author having repo write access; on failure, routes to auto-fixpipeline-triage.yml- Scores review findings, routes to auto-fix or human reviewpipeline-fix.yml- Claude agent auto-fixes simple review findingsdispatch.yml- Routes/triage,/fix,/pipelineslash commands to downstream workflowsfeature-request-triage.yml- Addsfeature-request-labeled issues to project boardrelease.yml- Builds and publishes macOS distributable to GitHub Releases on version tag push (v*)
Architecture
Cross-Platform Design
The app is designed to run both as an Electron desktop app and as a standalone web app. All platform-specific code is abstracted behind the ElectronAPI interface:
- Electron: Uses IPC to call main process for file dialogs, settings persistence (
app.getPath('userData')/settings.json), and LLM API calls - Web: Uses File System Access API (with
<input>fallback), localStorage for settings, and direct API calls (limited by CORS — Anthropic blocks browser requests, so web mode LLM features are unavailable)
Always use getApi() from src/renderer/lib/browserApi.ts instead of accessing window.api directly. This returns the Electron API when available, or a browser-compatible fallback.
Electron Process Model
When running as Electron:
- Main Process (
src/main/): Node.js environment handling window management, native file dialogs, and LLM API calls. LLM calls run here to avoid CORS issues. - Preload (
src/preload/index.ts): Context bridge exposingElectronAPIto the renderer viawindow.api. - Renderer (
src/renderer/): React app with no direct Node.js access.
IPC Channels
Most handlers live in src/main/ipc.ts (86 across 14 namespaces). A few sit closer to their feature: sentry:setEnabled and renderer:ready in src/main/index.ts, updater:* (3) in src/main/updater.ts, file:watch:start and file:watch:stop in src/main/fileWatcher.ts, and mcp:tool:result (via ipcMain.on, not handle) in src/main/mcp/bridge.ts.
file:*(22) - File operations (open, save, read, rename, delete, trash, duplicate, watch, export HTML, MAS bookmark activation, etc.) — 20 handlers inipc.ts, plusfile:watch:startandfile:watch:stopinfileWatcher.tssettings:*(4) - Settings persistence, secure storage check, API key testllm:chat,llm:fetchModels,llm:stream,llm:stream:abort- LLM API calls (streaming via Anthropic SDK)remarkable:*(20) - reMarkable tablet sync (register, validate, sync, sync:abort, OCR, folder move, etc.)mcp:*(3) - MCP server status, install, uninstall for Claude Desktopshare:*(21) - Gateway sharing: sign-in, access, publish/republish/revoke, local list +share:listRemotegateway reconcile (#1037), comments pull/push, path updates. Only the two local reads (share:list,share:getForPath) are callable on MAS buildssentry:setEnabled- Toggle Sentry error tracking from rendererupdater:*(3) - Auto-update check, download, install (Electron auto-updater)skill:download- Download an external skill for use inside Proseclipboard:*(1) - Write text to system clipboardappearance:*(1) - Pin the macOS dock tile to the Prose iconwindow:*,shell:*,recentFiles:*,emoji:*,fileAssociation:*- Utility handlers
State Management
Editor state is not in a store. Every open tab owns a ProseMirror EditorState held by a session in src/renderer/document/session.ts (see "Editor" below); the tab record in tabStore is the truth for a document's body, frontmatter, dirty flag, preview / read-only flags and rich-or-source mode; annotation records, comment threads and suggestions live in the plugins' own state.
Zustand stores in src/renderer/stores/:
tabStore- The tab records (Tab: identity,content,frontmatter,isDirty,isPreview,readOnly,mode,viewState), the closed-tab history, session persistence to IndexedDB;useActiveTab(select)reads the active recordeditorStore- A mirror of the active tab's record (document: id, path, content, frontmatter, dirty; written only by the session layer andtabs/actions.ts) plus UI flags (isEditing,isRemarkableReadOnly,isAutosaving,annotationsVisible, the AI frontmatter overlaypendingFrontmatter, the status-bar cursor). Nothing computes on the mirror.editorInstanceStore- TipTap editor instance referencechatStore- Chat messages, conversations, streaming state, panel visibilitysettingsStore- App settings (theme, LLM config, editor preferences)fileListStore- File explorer state and directory listingreviewStore- Review mode (quick/side-by-sidefor AI edits,commentsfor comment threads), suggestion navigationsummaryStore- AI-generated document summaries, staleness trackingcommandHistoryStore- Per-tool argument history, persisted to IndexedDBlinkHoverStore- Currently hovered link URL for tooltipnotificationStore- Bespoke in-app toasts (the app uses no toast library; rendered bycomponents/layout/Notifications.tsx)sharedListStore- Every local share entry (live + revoked) for the explorer's Shared view and the ◎ indicator on shared tree rows (#1036)shareStore- The active document's share entry and sync status (the ◎ popover)accountStore- Gateway sign-in stateprojectsStore- Project roots for the explorer
Feature Flags
src/renderer/lib/featureFlags.ts gates features that aren't ready for public release. Flags are persisted in settings.json (in app.getPath('userData')) under the featureFlags key. Each flag has its own default — see below.
To override a flag without rebuilding, add to ~/Library/Application Support/Prose/settings.json (macOS; Prose Dev/settings.json for npm run dev):
"featureFlags": { "remarkable": false }Current flags:
remarkable— reMarkable tablet sync (v1.1). Default: on for desktop builds; forced off for MAS builds regardless of the user's setting, since the reMarkable Cloud auth and OCR Lambda credentials aren't ready for App Store review. Set tofalseexplicitly to disable on desktop.webPlatform— sharing via the Prose gateway (share links, live comments, sign-in). Default: on for desktop builds since 1.8.1 (#943); forced off for MAS builds. Publishing is still gated server-side by theshare_publishgrant; users without one get Request access (#1018). Read it throughwebPlatformOn/useWebPlatformEnabled/isWebPlatformEnabled— neverfeatureFlags.webPlatformdirectly — so the default lives in one place.
The module exports React hooks (useRemarkableEnabled, useWebPlatformEnabled) for use in components and non-hook accessors (isRemarkableEnabled, isWebPlatformEnabled) for use in callbacks. When gating a feature, import the appropriate function and use it to guard UI rendering, background effects, and menu handlers. Preserve all code — gate, don't delete.
IndexedDB Schema Changes
Client-side persistence uses IndexedDB (src/renderer/lib/persistence.ts). When modifying the schema:
- Always bump
DB_VERSIONwhen adding/removing object stores. Theonupgradeneededcallback only runs when the version increases—reopening at the same version won't create missing stores. - Upgrades can be blocked if the database is open in another tab. The upgrade waits until all connections close, but users may not notice. New stores may silently fail to create.
- Test fresh installs and upgrades separately. A fresh install always gets the latest schema, but existing users need the migration path.
Editor-state rules (comments, AI suggestions, AI annotations): marks and
decorations are NOT serialized into the .md; the records live in IndexedDB
keyed by documentId and are re-applied at load by matching the marked text.
Four rules keep them safe, each enforced by a mechanism rather than a
convention (design: docs/architecture/editor-state.md §3.3; the tripwires in
e2e/electron.document-sessions.spec.ts fail the build when one is broken):
- No write without the producing state's document id. The three IndexedDB
writers (
saveAnnotations,saveComments,saveSuggestionsand their deletes) are imported only bydocument/session.ts, whosepersistRecordsreads the id and the records from the sameEditorState. - Id and records change together. They are fields of one immutable
EditorState(the identity plugin and each record plugin's state): a state is created whole and replaced whole, so no moment has the next id with the previous records. - No timing flag guards shared state. Load-time work is a pure function that returns a finished state: marks are applied inside the session's seed transaction before a state is ever shown, a reload re-anchors inside the replace transaction, and a record is added in the transaction that creates it. Nothing polls for "restored".
- Design before touching shared editor state. Only
session.tscallsview.updateState, creates anEditorStateor persists records; no renderer code callscommands.setContent(the session builds states). A new writer or projection is a function insession.ts, added in a PR that cites the design (docs/patterns.md, "How to Add a Session Writer or Projection").
LLM Integration
Anthropic is the only provider. All legacy multi-provider code (OpenAI, OpenRouter, Ollama) has been removed. The Settings type defines provider: 'anthropic' only.
LLM calls flow: useChat hook → getApi().llmChatStream() → IPC → main process → Anthropic SDK (with tools) or Vercel AI SDK (without)
Gating AI entry points: any UI or background path that triggers an LLM call must gate on the single source of truth in lib/llm.ts — isAIConfigured(settings) / aiAvailability(settings) (consent and valid config), or the useAIConfigured() hook in components. Disable user-initiated controls with an explanation when unavailable, fire notifyAINotConfigured() for blocked actions whose surface may be hidden, and silently no-op auto-fired calls (e.g. summaries). Never destroy user content (comments, suggestions) on a blocked call.
Tool pipeline, stream lifecycle, and tool modes: see docs/architecture/llm-pipeline.md.
Settings
Settings stored at app.getPath('userData')/settings.json (~/Library/Application Support/Prose/ on macOS; npm run dev uses its own Prose Dev/ profile, seeded once from Prose/ — see docs/troubleshooting.md). All settings path resolution is centralized in src/main/paths.ts. Default settings defined in src/main/ipc.ts. The Settings type in src/renderer/types/index.ts is the source of truth for the settings shape.
Legacy ~/.prose/ data is migrated automatically on first launch via src/main/migrate.ts (settings and dictionary only; credentials require re-entry due to safeStorage identity differences).
Theme
Dark mode by default. Theme controlled via dark class on <html> element.
Tabs
Every way a document enters a tab, leaves it, or changes identity goes through one function in src/renderer/tabs/actions.ts (open, preview, new, switch, close, reopen, rename, convert, reload from disk, restore at launch, setTabFrontmatter, markTabSaved); hooks/useTabs.ts is a thin React binding over it and the chat tools (select_tab, open_file, new_file, save_file) call the same functions. The tab record (tabStore) is the truth for the document's body, frontmatter, dirty flag, preview / read-only flags and mode; the session layer keeps it current from its projections. Session persistence via IndexedDB — tabs survive app restart; only the active tab's session opens at launch, the others on first activation. Preview tabs (single-click in file explorer) are replaced by the next open; double-click, clicking into the editor or editing promotes to a permanent tab, which re-applies editable = !isPreview && !readOnly.
Editor
The editor uses TipTap (ProseMirror-based) with markdown support via tiptap-markdown. There is one TipTap editor and one EditorState per open tab: src/renderer/document/session.ts holds the states (openSession parses, normalises and seeds a state from the tab's text and its IndexedDB records; activateSession swaps it into the view with view.updateState; applyToSession runs a transaction on a shown or a background state; replaceSessionDocument is the one whole-document replace, re-anchoring every record by text; setSessionIdentity restamps on rename / Save As; saveSnapshot is the one save path), the identity plugin (document/identity.ts) stamps each state with its document id, and document/handle.ts gives tool executors a DocumentHandle for a shown or a background document. Projections (500 ms after the last keystroke) write the tab record and the editor-store mirror; records persist from the state that produced them. The dirty flag follows every source-mode transaction at once, and a close or disk-reload decision lands the pending projection before it reads the record (landProjections, #885). Source mode keeps a CodeMirror state per session (extensions/source/state.ts). Key files:
src/renderer/document/session.ts- The session layer (the only creator, swapper and persister of editor state)src/renderer/components/editor/Editor.tsx- The editor component (keyboard, popovers, dialogs, the source surface)src/renderer/components/editor/extensions.ts- The extension list (createEditorExtensions), shared with the vitest harnesssrc/renderer/extensions/- Custom TipTap extensions (ai-annotations, comments, ai-suggestions, node-ids, source, etc.); records live in plugin state, read withannotationsOf(state),threadsOf(state),getAISuggestions({ state }), written through the extensions' commands or their…To(tr, …)helpers
Document Review
AI-generated edits can be reviewed before accepting. Two modes:
- Quick review — inline diff with per-change accept/reject controls
- Side-by-side — full diff view with navigation
Comment threads have their own top-level review surface, Comment Review
(CommentReviewPanel) — a peer of Quick Review managed through the same
reviewStore mode slot (reviewMode === 'comments'), so the two are mutually
exclusive and toggle cleanly. Prev/next navigation wraps in all review panels.
Managed by reviewStore and components in src/renderer/components/review/.
reMarkable Integration
Syncs handwritten notebooks and typed-text documents (Type Folio keyboard input) from reMarkable tablets. Located in src/main/remarkable/:
client.ts- reMarkable cloud API client (usesrmapi-js)sync.ts- Notebook sync logic with parallel downloads (3 concurrent), incremental metadata saves, and structuredSyncProgressUpdateprogress events.processNotebookContentderives markdown per page: typed text locally, handwriting via OCRocr.ts- Handwriting recognition via external OCR service (batch size 5, retry with exponential backoff)rm-scene-parser.ts- Zero-dependency parser for reMarkable's v6.rmbinary format; extracts typed text (RootTextBlock) and detects strokes. Ported from the MIT-licensedrmscene. Never throws — parse failures fall back to the OCR path
Typed text vs. handwriting: Typed docs are stored as fileType === 'notebook' (there is no separate "text" fileType) with digital text in the v6 .rm scene, so OCR renders them blank. processNotebookContent parses each page for typed text (no OCR/API key needed) and OCRs only stroke pages; typed text wins per page for mixed notebooks. The extraction metadata field (typed-text | ocr | mixed) records the source, and re-processing a legacy entry (no extraction field) once heals already-synced typed docs that had empty bodies.
Sync progress push: The remarkable:sync IPC handler pushes SyncProgressUpdate events via event.sender.send('remarkable:sync:progress', ...). The preload exposes onRemarkableSyncProgress(callback) for the renderer to subscribe.
OCR service: Requires environment variables REMARKABLE_OCR_URL and REMARKABLE_OCR_API_KEY pointing to the Lambda endpoint. When absent, OCR silently skips — handwriting notebooks have no markdown text, but typed-text docs still sync (typed extraction is local and needs no OCR). Also requires an Anthropic API key (reused from LLM provider settings, or configured separately in Settings → Integrations).
MCP Server
Prose exposes itself as an MCP server to Claude Desktop via two components:
src/main/mcp/- Socket server + HTTP bridge inside the Electron main process. Relays tool calls to the renderer via IPC.src/mcp-stdio/- Standalone stdio server that Claude Desktop launches. Connects to the running app via Unix socket; auto-launches Prose if not running.
Exposes 17 tools (the mcpToolNames subset in src/shared/tools/registry.ts): read_document, get_outline, open_file, suggest_edit, create_and_open_file, list_comments, add_comment, resolve_comment, reply_to_comment, reopen_comment, list_tabs, select_tab, and the review lifecycle (#890) list_suggestions, reply_to_suggestion, revise_suggestion, get_review_status, list_review_events. Accept and reject are never on the list: the writer decides (#607), and agents follow decisions through list_review_events. The document tools take an optional document (a documentId or path from list_tabs) that must name an open tab (#1237). MCP calls never move the writer's caret, focus or tab (tab-changing tools aside), and the renderer re-checks the allowlist the main-process bridge enforces. Every record an MCP call makes carries an Actor (src/shared/review/actor.ts, #1232), and the review log (extensions/review-log/, #1233) keeps each suggestion's history and the events.
Shared Code
src/shared/ contains code imported by both main and renderer processes:
tools/- Tool registry, Zod schemas, type definitions, mode-based access controlllm/models.ts- LLM model definitionsutils/retry.ts- Retry with exponential backoff
Sentry Error Tracking
Opt-in crash reporting via @sentry/electron. Users enable it in Settings > General > "Error Reporting".
- Main process:
src/main/sentry.ts—initSentry()called synchronously at startup by reading settings (checks new userData path first, falls back to legacy~/.prose/).setSentryEnabled()handles runtime toggle viasentry:setEnabledIPC. - Renderer:
src/renderer/lib/sentry.ts—initRendererSentry()called fromsettingsStore.loadSettings(). Uses dynamicimport('@sentry/electron/renderer')to keep the SDK off the critical render path (required forsandbox: truecompatibility).ErrorBoundarywraps<App />inmain.tsx. - DSN: Injected at build time via
SENTRY_DSNenv var. Falls back to the project DSN if unset. Renderer uses__SENTRY_DSN__(Vitedefine); main process usesprocess.env.SENTRY_DSN. - Privacy: Sentry never initializes unless
errorTracking.enabled === truein settings. In dev mode, Sentry is initialized but disabled (enabled: false). - Source maps: Uploaded automatically when
SENTRY_AUTH_TOKENis set during build (via@sentry/vite-plugin).
Common Patterns
Step-by-step recipes for common extension tasks (settings tab, IPC channel, TipTap extension, panel, Zustand store, AI tool, session writer or projection): see docs/patterns.md.
Security Rules
- Path handling — every filesystem IPC handler declares its path source in a comment (
// Path source: A|B|C …, seesrc/main/paths.ts) and validates accordingly. A (user-chosen: dialogs, explorer entries, the current tab, settings roots) →normalizePath(). KNOW WHAT IT IS: a normalizer, NOT containment —normalize()resolves..before the traversal check, soa/../../etc/xpasses clean. B (content-derived: document text, image srcs, imported artifacts,prose://payloads, LLM/MCP tool arguments, sync metadata) →resolveWithin(root, path, …)with an explicit containment root; it realpaths both sides and applies extension / hidden-folder rules (file:readBase64— audit H-03; tool paths useresolveToolPath(): text files under home or a user-chosen workspace root — default save directory, projects, favorites — never inside a hidden folder). C (a user-chosen directory + a typed name) → sanitize the name in the renderer withsanitizeEntryName()before joining. Never citenormalizePath()as a traversal defense. (#918) - API keys — store via
credentialStore(OSsafeStorage), never in plaintext. IfsafeStorageis unavailable, keys are stripped, not saved. - No
innerHTMLwith dynamic data — use JSX ortextContent. LLM-generated content must never be inserted as raw HTML. - Sandbox settings —
contextIsolation: true,nodeIntegration: false— never change these. - External URLs —
shell.openExternalonly allowshttp:andhttps:protocols. All others are silently dropped. - CORS — MCP HTTP server only reflects
localhost/127.0.0.1origins. No wildcardAccess-Control-Allow-Origin. - DevTools — stripped from production View menu. Set
PROSE_DEBUG=1env var to re-enable for debugging. - Privacy manifest —
resources/PrivacyInfo.xcprivacydeclares required-reason API usage for Apple. Update when adding new system APIs. - MAS sandbox — MCP install/uninstall/status handlers are blocked via
IS_MAS_BUILD. Settings path usesapp.getPath('userData')(sandbox-safe). Never usehomedir()+.prosefor new data paths. - App Store submission — NEVER submit builds for App Store review. Upload to App Store Connect / TestFlight is the automation boundary. Submission for review is always a human decision and action.
Sentry Debugging
Prose uses opt-in Sentry error tracking. When investigating production errors, use the Sentry CLI (npx sentry) — not MCP.
Playbook:
npx sentry issue list # List recent unresolved issues
npx sentry issue list --json | jq # Machine-readable output
npx sentry issue explain PROSE-<id> # Seer AI root cause analysis (takes ~1 min)
npx sentry issue plan PROSE-<id> # Generate a fix planFirst-time setup: npx sentry auth login (browser OAuth, stores token locally).
Workflow: issue list → issue explain <id> → fix in code → verify. Seer provides reproduction steps, suspect lines, and scoping analysis — treat it as a strong lead, not gospel. Always cross-reference against the actual source before committing a fix.
Troubleshooting
Common failure recovery (port conflicts, LevelDB locks, build failures, API errors, Circuit Electron, tool execution errors): see docs/troubleshooting.md.
UI Conventions
- Component library: Use shadcn/ui components. Don't introduce other UI libraries.
- Animations: Keep minimal. Use only what shadcn/ui and tailwindcss-animate provide out of the box.
- Fonts: IBM Plex Mono for all text (UI and editor) by default. Users can change fonts in Settings → Editor.
Session Management
When the user signals they want to stop working (e.g., "let's take a break", "let's stop for now", "I'm done for now"), automatically provide:
- A brief 1-2 sentence summary of what we were working on (to make resuming easy)
- The current date and time
This can also be invoked manually with /break.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

