Halfday.dev992 downloadsBorn-encrypted notes for Obsidian. age (X25519) at rest, live-preview markdown in memory.
X25519 age encryption for Obsidian notes.
Open a .age file in your vault and Halfday Rune decrypts it in memory, mounts a CodeMirror editor on the cleartext, and re-encrypts on save. Plaintext never touches disk for born-encrypted notes; for converted notes it lives on disk only as ciphertext.
🌐 halfday.dev/products/rune — full product page, screenshots, and the launch story.
Status: pre-release. The plugin is in active development by halfday. v0.7 will submit it to the Obsidian Community Plugins catalog.
Your Obsidian vault is plaintext on disk. That used to be fine. In 2026 it isn't: AI desktop tools (Claude Desktop via MCP filesystem servers, Cursor's codebase indexing, ChatGPT's macOS Work-with-Apps), cloud sync providers, indexers like Microsoft Recall, and the growing pile of agents with disk access can all read your notes. Obsidian Sync is end-to-end encrypted in transit but the local files are plaintext.
Rune encrypts at rest using age. Plaintext exists only in editor memory while a note is open; close the tab and it's gone from the process. New notes can be born-encrypted (plaintext never on disk at all). Multi-recipient support means you encrypt to both your daily-driver key and a backup, so losing one device doesn't lose the vault. Rotate-keys re-encrypts every .age file to your current recipients when you change the set.
Full case in the launch post: Using Obsidian Securely in the AI Era.
.age files inline. A custom view decrypts the file with your age identity and shows the cleartext in a normal-looking editor.cmd-S (or 30s of inactivity) encrypts the buffer, round-trip-verifies, then overwrites the .age on disk. If verification fails, the on-disk file is left untouched.Halfday Rune: Test round-trip (X25519) — proves your keys + the typage WASM stack work without touching any files.Halfday Rune: Encrypt current note → .age — seals an existing .md to .md.age, deletes the plaintext after round-trip verify.Halfday Rune: New private note — born-encrypted .age file; plaintext never on disk.Halfday Rune: Decrypt current .age → .md — inverse of the encrypt command. Two modes: replace (delete .age after writing .md) or scratch (keep both).Halfday Rune: Rotate vault keys — re-encrypts every .age file in the vault to your current recipients list. Optional pre-rotation tar.gz backup.manifest.json, main.js, and styles.css from the latest release (TBD: link will go live with the v0.7 submission).<your-vault>/.obsidian/plugins/halfday-rune/.Once submitted and approved, install via Settings → Community plugins → Browse → Halfday Obsidian Rune.
Halfday Rune needs an age recipient list and a matching identity on disk. The defaults match what the halfday CLI sealer uses, so if you already use seal.sh you're done.
# generate an X25519 keypair
mkdir -p ~/.age && chmod 700 ~/.age
age-keygen -o ~/.age/vault.identity
chmod 600 ~/.age/vault.identity
# extract the public recipient line into recipients.txt
grep '^# public key:' ~/.age/vault.identity \
| sed 's/^# public key: //' > ~/.age/recipients.txt
~/.age/recipients.txt is the canonical recipient list. One age1... recipient per line; #-prefixed lines are comments. Adding a backup recipient is just adding another line — the plugin will encrypt new notes to all listed recipients, and Rotate vault keys updates already-sealed files.
Halfday Rune's settings tab has these fields (the first three are desktop-only; the mobile ones are described under Using rune on your phone):
~/.age/recipients.txt.~/.age/vault.identity..age file to ~/halfday/logs/age-backups/ before running Rotate vault keys.There's also an inline "Recipients (file content)" editor — paste your age1... lines, save, and the plugin writes them to disk verbatim (preserving comments and ordering). Save validates the whole file before writing; malformed input refuses to save with the offending line called out inline.
Rune only encrypts to a set of keys you have confirmed. The first time it encrypts (or the first unlock on a phone) it shows every key, the count and a fingerprint, and asks. After that, every unlock and every save checks the current set (your recipients file plus your own key, or the list inside the unlock copy) against the confirmed one. Any difference stops it with "your recipients differ from the set you confirmed (+N added, −M removed)" and nothing is encrypted. A key that some other program appended to recipients.txt is never used silently. Saving the recipients editor asks again when you added a key, and updates the confirmed set without asking when you only removed keys. After changing the file any other way, run Halfday Rune: Confirm recipient set; it lists keys new since the last confirm separately.
The confirmed set lives in the plugin's settings (data.json) with a check value made from your private key, so changing it takes your key, not just write access to a file. Every confirm also bumps a counter inside that check. Each device keeps its own record of the newest confirmation it has seen (and whether it uses passphrase unlock) in Obsidian's local storage for the vault, which does not sync. A confirmation made on another device carries a higher counter and is accepted, so changing your keys on the Mac doesn't look like an attack on the phone. If the settings' copy is deleted, replaced with something that doesn't check out, or rolled back to an older confirmation, or the device's record is damaged, rune refuses with "your confirmed recipient set was removed or replaced" instead of asking as if it were the first time, and Confirm recipient set shows what differs from that record. An unlock copy that still holds keys you removed is refused as older than your confirmed set; make a new one on desktop. Only a device with no record at all is asked from scratch, so removing both the settings and Obsidian's local storage, then restoring an older unlock copy you made, still brings back a first-time prompt for that older set: read every key before you confirm. Every key in the prompt is shown whole and labelled "your key" or "other key".
Rune runs on Obsidian mobile. Your key never leaves your desktop in the clear: you make a passphrase-protected copy of it, it syncs with the vault, and the phone unlocks it once per session and keeps it in memory only.
Halfday Rune: Create mobile unlock copy. Choose a long random passphrase (20+ characters, from a password manager). Rune writes _rune/identity.age in your vault and checks it by unwrapping it again before reporting success. If the file already exists you are asked before it is replaced. The path is the Mobile unlock file setting._rune folder is a normal visible folder so Obsidian Sync and iCloud carry it. If you use Obsidian Sync, enable "Other file types" so .age files sync..age note, or run Halfday Rune: Unlock rune. Enter the passphrase (a password manager can fill it). A wrong passphrase shows "Wrong passphrase" and lets you retry. Notes you write on the phone are encrypted to the same recipients desktop uses (the list is stored inside the passphrase-protected file), so they open on desktop and with your backup key as usual.Halfday Rune: Lock rune now, or tap the "rune: unlocked" status-bar item. Open notes switch to a locked screen and their decrypted text is discarded. Two settings lock automatically:0 = never.0 = lock the moment the app is backgrounded.Rotating the key and editing recipients stay desktop-only. The unlock file holds your key and your full recipients list (a small rune-mobile-key v1 text payload: the identity plus one recipient: age1... line per recipient, all inside the passphrase-encrypted file, so nobody can add a key to the phone's list without the passphrase). If your own key's recipient is not in your recipients file, rune adds it to the copy and says so. Re-run Create mobile unlock copy whenever you change your recipients; the success message tells you what changed (recipients updated: +N / −M) when you reuse the passphrase. Unlock files from earlier builds hold the bare key only; unlocking one says to re-run the command.
Limits, stated plainly. The phone cannot tell which keys a note's header was made for, only how many, so it cannot detect that you swapped a recipient on desktop. If you forget to re-run Create mobile unlock copy, a phone save encrypts to the OLD list: the new key loses access to that note and the removed key gets it back. To catch this, the command also writes a public sidecar (_rune/identity.recipients.sha256, a hash of the recipients list) and desktop shows a persistent warning on load, and after you save the recipients editor, when it no longer matches (or is missing). The sidecar is advisory only: editing or deleting it can only hide or trigger the warning, never change what gets encrypted. Also note that a phone save widens a note to the full wrapped list: a note that had fewer recipients gains all of them.
Before saving an existing note, the phone checks that your unlocked key opens it, then re-encrypts it to the whole wrapped list. If the note on disk has more recipient stanzas than the list (for example you added a recipient on desktop after making the copy), the save is refused with "This note is encrypted to keys your phone doesn't know about — edit it on desktop, or re-run Create mobile unlock copy" and nothing is written. Rotate and backup skip the unlock file itself. The unlock file must use a scrypt work factor of 16 to 19; rune writes 18.
The unlock file in your vault is useless without its passphrase, but anyone holding the file can guess the passphrase offline with no rate limit, which is why it must be long and random. If you lose a phone:
Create mobile unlock copy again with a NEW passphrase and confirm the replacement. The old passphrase stops working on new copies of the file. This does not help against someone who already has a copy of the old file plus the old passphrase: assume that person has your key.Rotate vault keys to re-encrypt every note, then run Create mobile unlock copy again so the phone copy holds the new key._rune/identity.age.Rotation does not reach old copies: earlier synced versions of your notes and any backups are still encrypted to the old key, so they stay readable with the old key even after a rotation.
.age file: click it in the file tree. The custom view decrypts to memory, mounts an editor, and saves with cmd-S (or after 30s).Halfday Rune: New private note, enter a filename, hit Create. Plaintext never touches disk..md: command palette → Halfday Rune: Encrypt current note → .age. The plaintext is replaced with <name>.md.age after round-trip verify..age back to plaintext: command palette → Halfday Rune: Decrypt current .age → .md. Modal asks whether to delete the .age after writing.Halfday Rune exists because plaintext on disk is the primary risk for a vault that contains personal reflection, therapy notes, or anything else you wouldn't paste into an LLM. The plugin treats your local disk as semi-trusted (the OS protects your home directory) but treats anything that touches a cloud-sync provider, an LLM context window, or another process's memory as adversarial.
.age files on disk are byte-compatible with the standard age CLI: ChaCha20-Poly1305 over an X25519 + HKDF key-agreement. The plaintext is recoverable only by an identity holder..age files are routed through Halfday Rune's custom view; they never reach Obsidian's MarkdownView, file cache, or backlinks resolver. There is no [[wikilink]] graph routing, no backlink, no full-text search index for sealed notes.javascript: or data: URLs as clickable affordances, refuses to transclude ![[embed]] references, and renders raw HTML (<script>, <iframe>, <b>, …) as literal text rather than parsed markup..age files first, and continues on per-file failure rather than aborting..age view is open. The whole point of the live editor is to manipulate cleartext; while a .age file is open in a Halfday Rune view, its content lives in the CodeMirror document and in the plugin's heap. Closing the view discards the buffer; the plugin never writes plaintext to a temp file, swap, or recovery file. Browser/OS memory dumps, debugger access, or a malicious extension running in the same Electron process can still see it.~/.age/vault.identity is unencrypted on disk. Optional: run "Create mobile unlock copy", then "Use passphrase unlock on this computer" (or the Unlock method setting). The desktop then unlocks like the phone, holds the key in memory only and locks on idle, and you can delete the identity file yourself once it works. A computer with no identity file can switch too: it shows the keys inside the unlock copy and you confirm them. Rotate and "Create mobile unlock copy" need key file mode. Malware running as you can still read the unlocked key from the app's memory. The threat model assumes your local user account is not adversarial. If disk theft is in scope, wrap the identity with age -p or use age-plugin-se (Secure Enclave) when that integration ships.therapy_2026_q1.md.age reveals "I went to therapy in Q1 2026" to anyone who can read your filesystem listing. Halfday Rune doesn't rename or scramble paths..age ciphertext, not plaintext, so confidentiality holds — but a sync provider with timing visibility can correlate edit patterns. If that's in scope, host your vault locally.A security plugin should be legible about every capability it uses. Here's the full list — including the ones an automated reviewer (correctly) flags — and why each is necessary:
fs). Halfday Rune reads your age identity from ~/.age/vault.identity and your recipient public keys from ~/.age/recipients.txt, and writes rotation logs and pre-rotation backups under ~/halfday/logs/. The identity lives outside the vault on purpose: storing your private key inside the encrypted vault it unlocks would defeat the encryption. Logs and backups stay out of the vault so cloud sync doesn't churn on recovery artifacts. On desktop this is the key source. Obsidian mobile has no such filesystem access, so the phone uses a passphrase-protected copy of the identity stored in the vault (_rune/identity.age, read through the Obsidian vault API) and holds the unlocked key in memory only; rotate, backup and the recipients editor stay desktop-only..age files via Obsidian's vault API so it can re-encrypt every sealed note to your current recipient set. It only ever reads .age paths; it does not read or index your plaintext notes.child_process and runs no shell commands. The pre-rotation backup is a plain Node fs file copy. (Earlier versions shelled out to tar; that was removed.)Everything above is auditable in the open-source code under src/.
Decrypted markdown flows through these passes before rendering:
<script>, <iframe>, <object>, <embed> ever evaluates. Inline tags (<b>, <i>, …) render as visible angle-bracket source, not as styled markup.javascript: and data: link schemes get no link affordance. No accent color, no underline, no hidden-URL trick. The raw [text](javascript:...) source stays visible verbatim so what you see is what's on disk. is replaced with a placeholder chip reading [image: alt — url]. Moving the cursor onto the span reveals the raw markdown for editing. No HTTP request fires. v0.6.3 does not distinguish local-vault images from remote — both are deferred uniformly.![[embed]] is inert. The ! prefix bypasses the wikilink decoration entirely. The text renders as literal prose. No transclusion is performed; transclusion would require resolving the embed against Obsidian's metadata cache, which the rest of the plugin spends a lot of effort avoiding.See SECURITY.md. In short: please report security issues privately (not via a public GitHub issue) and we'll get back to you.
Rune is free and open source, and always will be. If it's part of how you keep your vault private, you can buy us a coffee — a thank-you for the work, not a feature gate. Security OSS only works when trust is the currency, so nothing is ever paywalled.
Apache 2.0. Copyright 2026 — see the LICENSE file for the copyright holder line.
Halfday Rune is built in TypeScript with obsidian-plugin-cli for the build, esbuild under the hood, and vitest for unit tests.
npm install
npm run dev # watch-mode build into the plugin folder
npm test # vitest run
npx tsc --noEmit # type-check
The plugin design plan and milestone history live in the vault_plugin_v0_plan document (link TBD).
Plugin scaffold initially generated by create-obsidian-plugin.