CLAUDE.md · TriliumNext/Trilium
CLAUDE.md from TriliumNext/Trilium: 295 lines covering Overview, Development Commands, Git Workflow, Monorepo Structure and more.
Template by TriliumNext
Chalk Labs
Need help deploying CLAUDE.md · TriliumNext/Trilium?
Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.
Install
Works with Claude Code. Read the repository before you give any tool access to your data.
curl -fsSL https://raw.githubusercontent.com/TriliumNext/Trilium/main/CLAUDE.md -o CLAUDE.md
Read it and adapt it to your project before use.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: prompt-engineering
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
What's in it
A CLAUDE.md file: project memory Claude Code loads at the start of every session.
- For
- Claude Code
- Length
- 295 lines
- File
CLAUDE.md
Sections
- Overview
- Development Commands
- Git Workflow
- Monorepo Structure
- Core Architecture
- Important Patterns
- Code Style
- Testing
- Documentation
- Recipes
- MCP Server
First lines (shown under the repo's AGPL-3.0 license)
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
Trilium Notes is a hierarchical note-taking application with synchronization, scripting, and rich text editing. TypeScript monorepo using pnpm with multiple apps and shared packages.
## Development Commands
```bash
npm install -g pnpm && pnpm install # setup (pnpm 12 is a native binary; corepack < 0.34.5 cannot start it)
pnpm server:start # dev server at http://localhost:8080
pnpm desktop:start # Electron dev app
pnpm standalone:start # standalone (in-browser) client
pnpm client:build | server:build | desktop:build
pnpm --filter <pkg> test <pattern> # e.g. pnpm --filter server test spec/etapi/search.spec.ts
pnpm --filter <pkg> coverage # per-package coverage (client, server, standalone)
pnpm typecheck # all projects, native compiler, a few seconds
pnpm dev:format-check | dev:format-fix # stylistic formatting
```
- **Never run ESLint** (`dev:linter-check`, `dev:linter-fix`, `npx eslint`) — it dies with an out-of-memory error, so a run tells you nothing. CI does not lint either — there is no `eslint`/`linter-check` step anywhere in `.github/workflows/`, so lint findings never block a merge; the gates are `pnpm typecheck` and the per-package `test --coverage` runs. (`packages/*` is ignored by the ESLint config anyway.)
- **Never run `pnpm test:all`, `test:parallel`, `test:sequential`, or a whole-package `coverage`** during development — CI runs them on every push. Run the **narrowest** suite that covers what you touched: `pnpm --filter <pkg> test <pattern>` (Vitest treats the trailing argument as a substring filter over spec paths). Core specs need **two** runs, server and standalone (see Testing). Only reach for a full suite if the user asks or wants a final check.
- **Typecheck with `pnpm typecheck`, not a raw `tsc`** — it resolves the project references a hand-written `tsc -p …` gets wrong, and is far cheaper than a test suite. Still, run it **once, after a piece of work is finished** — not after every edit: it builds the project references, and repeating it is a common way to lose minutes across a session. Two ways its output misleads: the incremental build does not recheck the files that use an ambient `.d.ts` (`types-lib.d.ts`) after it changes — delete `apps/client/dist/*.tsbuildinfo` (gitignored) before trusting the result; and a dependent project (`tsconfig.spec.json`, `apps/build-docs`) is checked only once the app project compiles, so its errors appear only after the app's are fixed, and some vanish with them.
- **Two TypeScript versions, on purpose**: root `package.json` has `typescript` 6.x (the JS compiler API that TypeDoc, typescript-eslint and the browser-bundled language service in `packages/codemirror` load) and `@typescript/native` (7, drives `pnpm typecheck` and owns the `tsc` bin). Do **not** bump `typescript` to 7, dedupe them, or switch to the `@typescript/typescript6` shim (no `lib.*.d.ts` → client build breaks). Reasoning: `docs/Developer Guide/Developer Guide/Environment Setup.md`, "TypeScript".
## Git Workflow
- **Committing directly on `main` is allowed and expected** for small fixes and self-contained features — do **not** create a branch first for those. The default "branch before committing on the default branch" rule does not apply to this repository.
- **Large or risky work goes on a branch**: multi-commit features, migrations, refactors spanning many packages, anything that needs review or a PR before landing.
- Only commit when explicitly asked to **in that message**, and the ask covers only the step it accompanies — a later step is left uncommitted until asked again. A remark like "commits go on main" is about branch choice, not standing permission to commit. Otherwise leave changes staged/unstaged for review.
- **The issue-closing keyword goes in the commit/PR subject**, not the body — `fix(markdown): wrap imported tables (closes #10270)`. A bugfix commit **must** carry it.
- **That keyword is the only way an issue is ever closed or annotated.** Never post a comment on a GitHub issue, never `gh issue close`, never change labels — the maintainer speaks to reporters, and the merge closes the issue. Reading issues (`gh issue view`) is fine; writing to them is not.
- **`git rm` is not a neutral delete.** It leaves the deletion staged, so a commit made before the matching reference updates are staged lands a `HEAD` pointing at a module that no longer exists. When removing a file whose references are edited in the same pass, delete it from the filesystem instead, or land the deletion and the reference updates together — never leave the index in a state that is broken on its own.
- **Commit by explicit path** — `git commit -F <message file> -- <paths>`, not whatever the index holds. The user stages and unstages in this worktree too, so between two commands the index can gain their files or lose yours. Give each commit its own message file: a `Write` onto an existing path that has not been read fails, and the commit then takes the old message.
- **Never run a tree-wide git operation — no `git stash`, `git checkout --`, `git restore`.** The user edits this worktree at the same time, so a stash takes their uncommitted work with it, and their IDE can flush a partial buffer onto the stashed state and block the pop. To decide whether a failure predates your change, argue statically instead — check whether the failing spec even imports the module you touched. If a real A/B is unavoidable, copy the file, edit it in place, and restore it, touching only files you wrote; prefer `git worktree add` on a temp dir over anything that reaches the whole tree.
### Commit messages
- **Format**: `type(area): imperative summary`, e.g. `fix(search): keep result paths out of the bookmarks clone (closes #10021)`. The area names the subsystem (`search`, `share`, `desktop`, `collections/table`), never a file; the types in use are `fix`, `feat`, `perf`, `docs`, `chore` and `build`.
- **Subject**: imperative mood, short (aim for 50 characters before any suffix), no trailing period, and it states the user-visible outcome rather than the mechanism.
- **Issue references go in the subject**: `(closes #N)` for a bugfix, `(#N)` when the issue stays open, `(regression of #N)` for a regression. The body never carries the closing keyword.
- **Body**: separated from the subject by a blank line, wrapped at 72 characters, and omitted when the subject says it all. Use it to explain what the user saw and why the code did it (naming the offending identifier), what changes and why that shape — including the alternative rejected — and what is deliberately unchanged or out of scope. Measurements, before/after numbers and red/green verification belong here, not in code comments.
- **AI attribution is an `Assisted-by` trailer, not prose.** When an AI tool wrote or materially shaped the commit, end the message with a blank line and `Assisted-by: <tool and model>` (e.g. `Assisted-by: DeepSeek V4.1 Flash`), one trailer per tool. `Co-Authored-By` is for human co-authors and `Signed-off-by` is the human's DCO certification, so neither names a tool; never announce the tool in the subject or as a `Generated with …` footer. A commit hook rewrites the trailer to name the model (`Assisted-by: Claude Code (Claude Opus 5.5)`); that is expected, so leave it alone and don't mention it to the user.
- **One logical change per commit**, each message self-contained: PR bodies are assembled from these bodies, so a message that only reads in sequence with the previous commit loses its rationale.
### Opening a PR
`git push -u origin <branch>` then `gh pr create --base main --title … --body-file <file>`. There is no PR template in `.github/`, so the body is yours to shape.
- **`origin` is `TriliumNext/Trilium`** — the working directory is named `Notes` and ~19 contributor forks are configured as remotes, so neither the path nor the remote list names the target. `gh` resolves it from `remote.origin.gh-resolved`, already set, so `--repo` is redundant; `upstream` is the archived `zadam/trilium` and is never a base.
- **Write the body from the commit bodies, grouped by what the change does** — `git log --format='=== %s%n%b' main..HEAD`. A feature branch here carries a long, self-contained rationale per commit (the measurements, the rejected alternative, the reason a value is what it is), and a PR description that restates the subject lines throws all of it away. Group the commits under headings by area, not chronologically: a reviewer reads the reworked subsystem, not the order it was built in.
- **The body describes the change, not how the branch got there.** No section on merges from `main`, conflict resolutions or rebases: they are branch bookkeeping, and the diff against `main` already holds their result. No "Testing" section either: CI runs the suites on every push, so a list of local `typecheck`/spec runs adds nothing. A red/green run that proves a fix belongs in that commit's body, and an untested gap goes in the chat reply to the user.
- **Pass the body via `--body-file`, not `--body`.** Markdown of this length in an argument hits shell quoting and heredoc-in-`fish` problems; write it to the scratchpad first.
- **Cosmetic `edit-docs` churn is ignored, not mentioned.** `pnpm edit-docs:edit-docs` (and `edit-docs:sync-docs`) reformats whole `apps/server/src/assets/doc_notes/**/*.html` files it merely opened (`<li><p>x</p></li>` ↔ `<li>x</li>`, ` ` shifted across a tag, attributes rewrapped), so a docs commit routinely touches pages the branch has nothing to do with. That is expected and needs neither a rebase nor a note in the description. Read the diff first, though: reflow is safe, but a changed `href`, a dropped `<img src>` or a paragraph that lost its text is a real defect and must be fixed before the PR goes up.
## Monorepo Structure
```
apps/
client/ # Preact frontend (+ legacy jQuery widgets); shared by server, desktop, standalone
server/ # Node.js backend (Express, better-sqlite3); serves the client, REST/WebSocket
desktop/ # Electron: server + client in one process (see developing-electron-desktop skill)
standalone/ # whole stack in the browser: core in a Web Worker on SQLite WASM + OPFS, no Node (see developing-standalone skill)
mobile/ # Capacitor shell around standalone (see developing-capacitor-mobile skill)
web-clipper/, website/, db-compare/, dump-db/, edit-docs/, build-docs/, icon-pack-builder/, script-deployer/
packages/
trilium-core/ # entities, services, SQL, sync, most API routes — shared by server, desktop, standalone
commons/ # types + utilities shared with the client
trilium-e2e/ # shared Playwright tests
ckeditor5/, codemirror/, highlightjs/, share-theme/, pdfjs-viewer/, splitjs/, turndown-plugin-gfm/
```
`pnpm --filter <package> <command>` runs a command in one package.
**`packages/trilium-core` is shared by server, desktop and standalone — *not* by the client.** `apps/client` has zero `@triliumnext/core` imports; it reaches the backend over REST/WebSocket and shares only **types** via `@triliumnext/commons`. So: a dependency added to core lands in server, desktop and standalone bundles (standalone's worker imports core at startup — that is the cost to weigh, never "the client would pay for it"), and frontend code can never call a core function — it needs an API route or a type in commons. The split is backend-vs-frontend, not Node-vs-browser: standalone runs core in a browser worker, which is why core carries the no-Node-built-ins rules below.
## Core Architecture
### Three-Layer Cache System
All data access goes through cache layers — never bypass with direct DB queries:
- **Becca** (`packages/trilium-core/src/becca/`): Server-side entity cache. Access via `becca.notes[noteId]`.
- **Froca** (`apps/client/src/services/froca.ts`): Client-side mirror synced via WebSocket. Access via `froca.getNote()`.
- **Shaca** (`apps/server/src/share/`): Optimized cache for shared/published notes.
**Critical**: Always use cache methods, not direct DB writes. Cache methods create `EntityChange` records needed for synchronization.
### Entity System
Core entities live in `packages/trilium-core/src/becca/entities/` (not `apps/server/`):
- `BNote` — Notes with content and metadata
- `BBranch` — Multi-parent tree relationships (cloning supported)
- `BAttribute` — Key-value metadata (labels and relations)
- `BRevision` — Version history
- `BOption` — Application configuration
- `BBlob` — Binary content storage
Entities extend `AbstractBeccaEntity<T>` with built-in change tracking, hash generation, and date management.
### Entity Change & Sync Protocol
Every entity modification creates an `EntityChange` record driving sync:
1. Login with HMAC authentication (document secret + timestamp)
2. Push changes → Pull changes → Push again (conflict resolution)
3. Content hash verification with retry loop
Sync services: `packages/trilium-core/src/services/sync.ts`, `syncMutexService`, `syncUpdateService`.
### Widget-Based UI
Frontend widgets in `apps/client/src/widgets/`:
- `BasicWidget` / `TypedBasicWidget` — Base classes (jQuery `this.$widget` for DOM)
- `NoteContextAwareWidget` — Responds to note changes
- `RightPanelWidget` — Sidebar widgets with position ordering
- Type-specific widgets in `type_widgets/` directory
**Widget lifecycle**: `doRenderBody()` for initial render, `refreshWithNote()` for note changes, `entitiesReloadedEvent({loadResults})` for entity updates. Fluent builder pattern: `.child()`, `.class()`, `.css()` chaining with position-based ordering. These legacy widgets are jQuery; new UI is Preact under `widgets/react/` — don't mix the two inside one component.
The client runs **Preact 11**: components, `createPortal` and types come from `preact`, hooks from `preact/hooks`, and `preact/compat` only for `memo`, `lazy`, `Suspense`, `flushSync` and `useSyncExternalStore`. Compat is nonetheless always loaded, so `onChange` on a text input fires on `input` and `onFocus`/`onBlur` bubble. A removed component's `useEffect` cleanup runs after paint, so one that must happen during the unmount is a `useLayoutEffect` cleanup. Ref typing, per-element ARIA types and the rest: the **`building-client-ui` skill**, "Preact 11 conventions"; for specs (unmount inside `act()`, which events to dispatch): **`writing-unit-tests`**.
#### Reusable Preact Components
Shared components live in `apps/client/src/widgets/react/` — **always** reuse them (`FormTextBox`, `FormSelect`, `Button`, `Badge`, `NoItems`, `Dropdown`, `Table`, `Calendar`, …) instead of writing raw HTML elements or a custom implementation, and never put Bootstrap utility classes (`form-control-sm`, `input-group`, …) on them. Any control floating **over** a note's content (map, mind map, image, diagram) goes on `OverlayControlGroup` / `OverlayToolbar` — never a hand-rolled `<button>`. The full catalogue, when to use a `Dropdown` menu and when a `DropdownPanel` and the overlay-control contract are in the **`building-client-ui` skill** — load it before building client UI.
#### Component Styling
- **Avoid inline styles** — do not use the `style` attribute/prop on JSX elements unless absolutely necessary (e.g. a truly dynamic, computed value that cannot be expressed in CSS). Static layout, sizing, spacing, and visual properties must go in CSS.
- **Per-component CSS files**: each component should have a matching `.css` file (e.g. `my_dialog.tsx` → `my_dialog.css`), imported at the top of the component file.
- **CSS nesting for scoping**: since CSS modules are not available, scope styles using a root class and native CSS nesting. For example, a dialog with `className="my-dialog"` should have its styles nested under `.modal.my-dialog { … }`.
- **Reuse existing components** instead of building custom markup — prefer `FormTextBox`, `FormTextBoxWithUnit`, `FormSelect`, `Slider`, `Button`, etc. over hand-rolled `<input>`, `<select>`, or `<button>` elements.
- **Safe-area insets** always read `var(--safe-area-inset-top, env(safe-area-inset-top))` (and the `-bottom`/`-left`/`-right` twins), never a bare `env()`. Android's WebView leaves `env(safe-area-inset-*)` at `0`, so `apps/mobile`'s `MainActivity` injects the real values as those custom properties; the `env()` fallback covers iOS and desktop browsers, where nothing is injected. Details in the **`developing-capacitor-mobile` skill**.
### API Architecture
- **Internal API** — REST, trusts the frontend. Most routes are core-shared and also run under WASM (`packages/trilium-core/src/routes/`); Node-only ones live in `apps/server/src/routes/api/`. Load the **`adding-internal-api-route` skill** for the wrapper/return conventions before adding one.
- **ETAPI** (`apps/server/src/etapi/`) — external API with token auth; keep it backwards compatible.
- **WebSocket** (`packages/trilium-core/src/services/ws.ts`) — real-time sync to the client (IPC-backed on desktop).
### Platform Abstraction and core rules
`packages/trilium-core/src/services/platform.ts` defines `PlatformProvider` (`crash()`, `getEnv()`, `isElectron`/`isMac`/`isWindows`), implemented per app in `apps/desktop`, `apps/server`, `apps/standalone`; singleton via `initPlatform()`/`getPlatform()`. Because core also runs in standalone's browser worker:
- **No `process.env`** — `getPlatform().getEnv(key)` (standalone maps URL params like `?safeMode` → `TRILIUM_SAFE_MODE`).
- **No Node built-ins** in core, including `path` — use `packages/trilium-core/src/services/utils/path.ts` (`extname()`/`basename()`) and the platform providers.
- **Platform checks are functions** — `isElectron()`, `isMac()`, `isWindows()` from `utils/index.ts` call `getPlatform()` and only work after `initializeCore()`; in static definitions wrap them in a closure (`value: () => isWindows() ? "0.9" : "1.0"`).
- **Avoid the barrel** in early-loading modules — `import { x } from "@triliumnext/core"` loads every export; `config.ts`-like modules import subpaths (`@triliumnext/core/src/services/utils/index`) to dodge init-order cycles.
- **Binary conversions** go through `packages/trilium-core/src/services/utils/binary.ts` (`wrapStringOrBuffer`/`unwrapStringOrBuffer` for `string | Uint8Array`, `encodeBase64`/`decodeBase64`, `encodeUtf8`/`decodeUtf8`; also exported as `binary_utils`), not hand-rolled `TextEncoder`/`Buffer.from()`.
### Electron Desktop App
`apps/desktop` runs server + client in one Electron process; the renderer loads over the `trilium-app://` custom protocol and talks to main only through the preload bridge (`window.electronApi`, typed by `packages/commons/src/lib/electron_api_interface.ts`). The main process bundles to **ESM with code splitting** (`dist/main.mjs` + lazy `chunks/`, via `buildBackend(..., { format: "esm" })`); the preload and `image_worker.cjs` stay CJS. `nodeIntegration` is off, `contextIsolation` is on, `@electron/remote` is gone — never `require("electron")` in client code. Adding an API means interface + `preload.ts` + an `ipcMain` handler in the owning service + a spec. Load the **`developing-electron-desktop` skill** for the recipe, the security model, running/launch errors and testing.
### Flathub packaging
`apps/desktop/flatpak/` holds the whole recipe — manifest, `trilium.sh`, `flathub.json`, `.desktop`, metainfo — and `scripts/flatpak/` writes it into the Flathub repo (`update-repo.mts` pins the ref and the pnpm sources, `generate-sources.mts` rebuilds the offline `generated-sources.json`); `.github/workflows/release-flathub.yml` runs both and opens the PR. **Never hand-edit the packaging repo** — the next run overwrites it. The manifest runs the checkout's own `apps/desktop/electron-forge/{flip-fuses,trim-locales}.ts` and `chore:update-build-info --from-commit`, each shared with the forge build, so **a change there lands in both the `.deb`/`.rpm`/Forge flatpak and Flathub** — keep the run-as-script guard and the argument the manifest passes. The sandboxed data dir is a **review condition**: `trilium.sh` reads `$XDG_DATA_HOME/trilium-data` and the Flathub reviewer struck every grant for the legacy `~/.local/share/trilium-data`, so existing users' notes are unreachable by design until the migration is built — never "fix" that by adding a `--filesystem=` back. Load the **`packaging-for-flathub` skill** before touching any of it.
### Standalone (in-browser) app
`apps/standalone` runs the client on the page and `@triliumnext/core` — plain JS — in a dedicated Web Worker over `@sqlite.org/sqlite-wasm` persisted in OPFS; a Web Lock elects the one tab that owns the database, which answers its own API calls straight from the worker (`standaloneApi.localFetch`); the service worker forwards other tabs' calls to it and covers what the page still requests. Every core provider has a browser twin in `apps/standalone/src/lightweight/` — **a new provider or Node import in core breaks this build first.** Load the **`developing-standalone` skill** before touching `sw.ts`, `main.ts`, `local-server-worker.ts`, `lightweight/*` or `vite.config.mts`.
### Mobile (Capacitor) appSignals
- Stars
- 38,533
- Forks
- 2,600
- Open issues
- 560
- Last commit (this folder)
- Oct 24, 2026 (7 days ago)
- Last release
- Oct 23, 2026 (8 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- AGPL-3.0
- Repository
- github.com/TriliumNext/Trilium
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
Done for you
Rather not set it up yourself?
Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.
See how we build AI agents →Read the case studies
Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.
- launches shipped
- 50+
- launches shipped
- media placements
- 500+
- media placements
- founders on every account
- 2
- founders on every account
Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block
Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.
Get CLAUDE.md · TriliumNext/Trilium running in your business
Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.
Cite this page
CLAUDE.md · TriliumNext/Trilium. Agent Mart. https://agentmart.dev/templates/triliumnext-trilium-claude-md (last updated 2026-10-24)