gjtorikian11 downloadsCapture keystroke evidence as you write and mint cryptographic proof-of-writing certificates via semoi.net.
An Obsidian plugin that captures keystroke evidence while you write and mints a cryptographic certificate (a "proof") of that session via semoi.net. The proof gets stamped into the note's frontmatter, and you get a link to a public verification page.
While you type, the plugin watches CodeMirror's change stream and records
each edit as an atom: a single ins (insert), del (delete), or rep
(replace) with a character count and a timestamp relative to the start of the
session. It does not record the characters you typed, only the shape of
your editing activity.
When you run the Mint proof for current note command, the plugin:
POST /proof endpoint.semoi: key.A "session" is a stretch of editing in one file. Sessions live in memory until you explicitly mint or reset them — there is no idle-rollover and no auto-mint.
Two clocks are tracked per session:
startedAt / endedAt — wall-clock bounds. endedAt - startedAt is how
long the session has been open, which is usually longer than you were
actually typing.activeMs — accumulated active typing time. Each gap between
consecutive keystrokes shorter than the active-typing threshold (default
5 seconds) adds to this counter; longer gaps don't. So if you type for two
minutes, walk away for an hour, then type for another minute, the proof
reports ~3 minutes of active typing across a ~63-minute window.If you delete the file, its in-memory session is dropped. If you rename the file, the session carries over to the new path so a mid-session rename doesn't lose captured keystrokes.
proofId
from frontmatter and asks the backend to verify it. Reports valid /
INVALID along with the signing key's status.| Setting | Notes |
|---|---|
| Active typing threshold (seconds) | Per-keystroke gap below which the time counts as active typing (default 5). Does not roll sessions over — minting stays explicit. |
The backend URL is baked in at build time (__SEMOI_BACKEND__ in esbuild.config.mjs): https://semoi.net for production builds, http://localhost:3737 for dev.
The proof reference is written into the note's frontmatter (one entry, or an array of entries if the note has been minted multiple times):
---
semoi:
proofId: <24-char hex id>
verifyUrl: https://your-backend/v/<id>
kid: <key id that signed it>
contentHash: <sha256 of note text at mint time>
issuedAt: <unix ms>
---
The full signed proof — signature, public key, and the full claim — lives on the backend and is fetched at verify time. The frontmatter entry is the local pointer to it.
The mint payload (MintPayload in src/session.ts):
docId — vault-relative file pathstartedAt / endedAt — unix ms (wall-clock bounds)activeMs — accumulated active typing time across the sessionevents[] — array of { t, k, n } (relative ms, kind, length)content — full note text (the backend hashes it; raw text is not stored)client — { name: "semoi-obsidian", version }npm install
npm run build # bundles to main.js for distribution
npm run dev # esbuild in watch mode
npm run typecheck
Drop main.js, manifest.json into
<vault>/.obsidian/plugins/semoi/ and enable the plugin in Community plugins.
src/main.ts — plugin entry, command wiring, editor extensionsrc/session.ts — framework-free session tracker and payload buildersrc/api.ts — SemoiApi HTTP client (/proof, /verify/:id)src/storage.ts — frontmatter persistencesrc/settings.ts — settings tab./scripts/release.sh patch # or minor / major / x.y.z
The script validates the working tree, runs typecheck + tests + build,
then npm version bumps package.json, manifest.json, and
versions.json, commits, and creates a bare signed tag (no v prefix —
Obsidian requires this). The push triggers .github/workflows/release.yml,
which rebuilds on CI, attests main.js build provenance, and uploads
main.js + manifest.json to a draft GitHub release. Review and publish
from the GitHub UI.