Mirror your notes to Trilium.
A VS Code extension that mirrors your notes workspace into Trilium via the ETAPI (Trilium's External API).
The local workspace stays the source of truth; Trilkeep writes a one-way copy into Trilium.
- One-way backup into Trilium. A batched full backfill on the first run, then incremental hash-diff runs that upload only changed files and never duplicate notes.
- Lossless Markdown. Files are stored as Trilium code notes (
text/x-markdown) byte-for-byte; folders become container notes that recreate your tree. - Backup on save (opt-in). Back up changed files automatically as you save.
- Backup on open (opt-in). Run a full backup when the workspace opens, to catch up edits made while it was closed.
- Read-only mirror (on by default). The mirror renders read-only in Trilium so
you don't accidentally edit it. You can make it editable (
trilkeep.readOnly: false), but the workspace stays the source of truth, so any changes you make in Trilium are overwritten on the next backup. (True two-way sync is a longer-term roadmap idea, not a current capability.) - Secure token storage. Your ETAPI token is kept in VS Code SecretStorage, never
written to
settings.jsonor any file you could commit by accident. - Zero runtime dependencies. Built on Node's
fetchandcrypto, with a 7-day dependency cooldown on the dev toolchain.
- VS Code 1.90 or later.
- Desktop VS Code. The extension uses Node APIs (filesystem + crypto), so it doesn't run in the browser-only web editor (vscode.dev / github.dev).
- A reachable Trilium server with ETAPI enabled (see Setup).
Install Trilkeep from the VS Code Marketplace or Open VSX, then:
- Run Trilium and open Options → ETAPI, then generate a token.
- In VS Code, run
Trilkeep: Setup, a quick wizard for the essentials (instance name, token, server URL, on-save). For the rest (globs, grouping, read-only, hard-delete, backup on open), runTrilkeep: Setup (Advanced), which walks every setting. Both pre-fill the current values, so you can re-run either any time to review or change config. The token is stored in VS Code SecretStorage (never in settings) and is never displayed. Give each Trilium instance a distinct instance name (the token + backup state are keyed by it, so the URL can change freely). Instance names are stable: pick an existing instance or create a new one, but you don't rename them (a new name is simply a new backup). - Run
Trilkeep: Back Up Workspaceto back up.
Prefer to configure by hand? Set trilkeep.serverUrl in VS Code Settings
(Cmd/Ctrl+,, switch to the Workspace tab so it saves per-repo like Setup
does, then search "trilkeep", or edit .vscode/settings.json directly), run
Trilkeep: Set ETAPI Token, then Trilkeep: Test Connection to confirm.
A state manifest at <workspace>/.trilkeep/state.json maps every backed-up
file to the Trilium noteId it became plus a sha256 of its content. That
manifest is what makes incremental backup possible:
- New file → create a Trilium note and record it.
- Changed file (hash differs) →
PUTnew content to the existing note. - Unchanged file (hash matches) → skipped.
- Removed file → logged, kept in Trilium by default (soft). Set
trilkeep.hardDeleteRemovedFiles: trueto delete instead.
Markdown files are stored as Trilium code notes with mime text/x-markdown
so the raw Markdown is preserved byte-for-byte (text notes would force a lossy
HTML conversion). Folders become container (book) notes, recreating the tree.
| Command | Action |
|---|---|
Trilkeep: Setup |
Quick wizard for the essentials: instance, server URL, token, on-save (re-runnable). |
Trilkeep: Setup (Advanced) |
Guided walk-through of every setting (re-runnable). |
Trilkeep: Back Up Workspace |
Full/incremental backup of the open workspace. |
Trilkeep: Back Up Workspace (Dry Run) |
Show what would be backed up (new/changed/unchanged/skipped/removed) without writing to Trilium. No token needed. |
Trilkeep: Test Connection |
Verify server URL + token via /app-info. |
Trilkeep: Set ETAPI Token |
Store the ETAPI token for the current instance. |
Trilkeep: Clear ETAPI Token |
Remove the stored token for the current instance. |
Trilkeep: Forget Instance |
Stop tracking any known instance: clears its (global) token and optionally this repo's backup state. Trilium is left intact. |
| Setting | Default | Description |
|---|---|---|
trilkeep.instanceName |
default |
Stable name for this Trilium instance (e.g. real, test). Keys the token + backup state, so serverUrl can change without losing them. |
trilkeep.serverUrl |
http://localhost:8080 |
Trilium base URL (just the address; change it freely). |
trilkeep.include |
["**/*.md"] |
Globs to back up. |
trilkeep.exclude |
node_modules, .git, .trilkeep |
Globs to skip. |
trilkeep.backupOnSave |
false |
Incremental backup on each save. |
trilkeep.backupOnActivation |
false |
Full backup when the workspace opens (the extension activates). Runs in the background; never blocks startup. |
trilkeep.rootNoteTitle |
(empty) | Title of this workspace's root note. Blank = the workspace folder name. |
trilkeep.group |
Trilkeep |
Slash-path of container notes to nest the backup root under (e.g. Trilkeep/work/repo). Blank = no grouping. |
trilkeep.parentNoteId |
(empty) | Existing Trilium noteId to use as the base parent instead of Trilium's root. Blank = Trilium root. |
trilkeep.readOnly |
true |
Mark the mirrored tree read-only in Trilium (inheritable #readOnly label). Backups still update it. |
trilkeep.hardDeleteRemovedFiles |
false |
Delete Trilium notes for removed files. |
trilkeep.excludeSecrets |
true |
Skip files whose name looks like a secret (.env, *.pem, id_rsa, credentials, …) even if include matches them. A manual backup lists any matches and lets you include them for that run; set false to always back them up. |
- Token lives in VS Code SecretStorage, not
settings.json, and is keyed byinstanceName(a stable name you choose), not byserverUrl. Distinct instances (e.g.testvsreal) never share a token, and changing a server's address, such as a churning LAN IP, never loses or misroutes the token. The backup-state manifest is keyed the same way (.trilkeep/state.<instance>.json), so two instances keep independent trees. (serverUrl/instanceNameare workspace-scoped settings; the token is global SecretStorage.) - Secret files are skipped by default.
trilkeep.excludeSecrets(on) keeps credential-looking files (.env,*.pem,*.key,id_rsa,credentials,.npmrc, …) out of the backup even whenincludematches them, so wideningincludeto**/*doesn't sweep secrets into Trilium. A manual backup lists any matches and lets you include them for that run; automatic runs (on save / on open) skip them, log each one, and show a once-per-session notice, so nothing is silently uploaded. It is a filename heuristic, not secret scanning: it misses a renamed secret and does not catch a secret living inside a tracked note (a.mdfile with a password), so still addtrilkeep.excludepatterns for anything sensitive it wouldn't recognize. Settrilkeep.excludeSecretstofalseto back these files up. - Zero runtime dependencies. Uses Node's built-in
fetchandcrypto, so there's no third-party supply-chain surface in what ships. - Dependency cooldown.
pnpm-workspace.yamlsetsminimumReleaseAge: 10080(7 days), so we never install a version published in the last week, which closes the window where a freshly-compromised release is live before it's caught. Dependabot is configured with a matching 7-daycooldown.
- Verified against Trilium 0.103.x, over localhost and LAN. Trilkeep uses the
ETAPI, Trilium's stable external API, so it should hold across Trilium versions
and over remote connections, but it has only been exercised against Trilium
0.103.x reached at
http://localhostand on a local network. Other versions and remote setups (HTTPS, a reverse proxy, or a tunnel) are expected to work but are not yet tested. Please report any trouble on a different version or a remote URL. - Remote instances may rate-limit large backups. A reverse proxy or CDN in
front of a remote Trilium (nginx, Caddy, Cloudflare, and the like) can throttle
the rapid sequential requests a big backfill makes. Trilkeep retries a throttled
or transient response (
429,502,503,504) up to three times, honoring the server'sRetry-Afterheader and otherwise backing off exponentially. (A429is retried for any request; a5xxonly for idempotent ones, so a create is never duplicated.) If it persists past those retries the file is reported as an error; re-running the backup picks it up (failed files aren't recorded, so nothing is silently skipped). Direct localhost/LAN connections have no such limit. - One window per workspace at a time. Overlapping backups are guarded within
a single VS Code window, but that lock doesn't span processes. If you open the
same folder in two windows and back up from both at once, they can race the
shared
.trilkeep/state.jsonand create duplicate notes in Trilium. Back up from one window at a time. - The mirror is not protected/encrypted in Trilium. Trilium's protected notes
are encrypted with a key from your protected-session password, which only exists
in an interactive UI session. The ETAPI token never holds that key, so the API
cannot create or modify protected notes (
isProtectedis read-only over ETAPI; writing to a protected note returnsNOTE_IS_PROTECTED). Trilkeep's notes are therefore stored unprotected, and unencrypted at rest, even if the originals you keep in Trilium are protected. Do not manually "Protect subtree" on the backup tree either: the next incremental backup would fail on every changed note. Your local.mdfiles are already plaintext on disk, so the copy matches the source, but it does not inherit Trilium's at-rest encryption.
Planned and under consideration (directional, not commitments):
- Automatic backup. Opt-in backup when the workspace opens, plus scheduled/periodic runs and a status-bar indicator (last backup time, in progress, errors).
- Standalone multi-repo daemon. Back up several repos at the OS level, beyond the one open in VS Code.
- Two-way sync (opt-in, longer-term). Pull edits made in Trilium back into the workspace, not just push. A distinct mode from the default one-way backup (which keeps the workspace as the source of truth); the hard part is conflict handling when a note and its file both change, plus tracking remote-side changes and deletes.
pnpm install # respects the 7-day cooldown
pnpm run typecheck # tsc against src + test
pnpm run lint # eslint
pnpm run test # node:test unit suite (pure logic)
pnpm run compile # build to out/Press F5 in VS Code to launch an Extension Development Host with the extension loaded.
Bug reports and feature ideas are welcome. Open an issue on the GitHub repo. If something feels clunky in daily use, describe your workflow so it can be reproduced.
MIT. See LICENSE.
