Jotura

Docs / The CLI / Watching and diagnostics

Watching and diagnostics

Reference for jotura watch, status, vault info, doctor, and conventions: the event stream, the polling model, every health check, and the exit codes.

This page covers the commands that report on a vault. jotura watch streams change events as they happen. jotura status and jotura vault info report totals and sync state. jotura doctor runs a list of health checks. jotura conventions prints the shared conventions note that agents read before they file anything.

Four of these five commands only read. conventions is the exception: it creates the conventions note from a bundled seed the first time you run it, so its first run writes to the vault. Every later run is a read.

Every command here accepts the four global flags that work at any level, described in Install and setup: --vault <PATH>, --json, -v or --verbose, and -h or --help. There is a fifth global flag, -V or --version, which works only at the top level. Which commands honor --json varies, and each section below says so. Errors are always a single line of JSON on stderr no matter what, as described in Exit codes and JSON output.

Shell examples on this page assume a POSIX shell such as bash or zsh. On PowerShell, adapt the quoting and the variable syntax.

Watching a vault for changes

jotura watch prints one JSON object per line to stdout for every change it sees in the vault. It runs until you stop it, or until the first poll cycle that matched, if you pass --once.

jotura watch

Watch flags

FlagTypeDefaultWhat it does
--paths <PATHS>glob stringnoneOnly report events whose path matches this glob. The glob is compiled with globset and matched against the vault-relative path, which always uses forward slashes. An invalid glob exits 9.
--kinds <KINDS>comma-separated stringnoneOnly report these event kinds. Accepts C or CREATED, M or MODIFIED, and D or DELETED, case-insensitively. Empty tokens are skipped. R and RENAMED are rejected, as is any other token. Both cases exit 9.
--oncebooleanfalseExit after the first poll cycle that emitted at least one matching event.

Both filters apply together. An event has to match the glob and be one of the allowed kinds before it is printed.

jotura watch --paths "Daily/**" --kinds C,M

The glob is not a shell glob. globset leaves its literal_separator option off here, so * matches the / separator as well as ordinary characters. That means --paths "*.md" also matches Daily/2026-08-30.md, and --paths "Daily/*" also matches Daily/sub/note.md. If you want a single level, spell it out with a character class or accept the recursive match.

--kinds R is an error rather than an empty filter. The poller cannot produce a rename event, so the flag rejects the token instead of accepting a filter that could never match:

rename events are not emitted; a rename appears as a Deleted plus a Created event (allowed kinds: C,M,D)

Any other unrecognized token gives a similar message, with the offending token quoted:

unknown event kind `X`; allowed: C,M,D

Both messages exit 9. In builds up to and including 0.7.0, R was accepted as a filter token that silently matched nothing. If you are scripting against an older installed binary, check which behavior you have before relying on the error.

--once is worth reading carefully, for two reasons. It does not stop after exactly one line: the command exits after the first poll cycle in which something matched, and a single cycle can carry several events. It also has no timeout. A --once run whose filters never match runs forever, so a build step that uses it needs its own timeout around the process.

jotura watch --once --kinds D

The event format

watch ignores --json and always writes compact newline-delimited JSON, one object per line. This is the only output form the command has.

{"kind":"Created","path":"notes/a.md","ts":"2026-08-30T09:14:03Z"}
{"kind":"Modified","path":"notes/b.md","ts":"2026-08-30T09:14:04Z"}
{"kind":"Deleted","path":"notes/c.md","ts":"2026-08-30T09:14:05Z"}
FieldMeaning
kindIn practice one of Created, Modified, or Deleted, capitalized exactly as shown. The emitter also has a Renamed branch, which this command never reaches.
pathThe vault-relative path of the file, using forward slashes.
tsThe time the line was emitted, as ISO 8601 UTC to second precision.

Those three fields are the whole object. The internal event structure the poller builds also carries a self_write flag, which is always false here and is never serialized, so it will not appear in the stream. A from field exists on the Renamed branch and is likewise unreachable from this command.

Events emitted within a single poll cycle are sorted by path. The comparison is a plain byte-wise string comparison, not a locale-aware one, so every uppercase initial sorts before every lowercase initial. Ordering inside a cycle is therefore by path, not by the time each change happened.

Because output is line-delimited JSON on stdout, it pipes cleanly into a line-oriented consumer.

jotura watch --kinds M | jq -r '.path'

Adding -v to a piped watcher is safe. The one verbose line it produces, vault: <resolved path>, goes to stderr, so the JSON stream on stdout stays clean.

How the polling model works

watch is a poller, not an operating system filesystem watcher. It takes a snapshot of every synced file in the vault, recording the path, the modification time in milliseconds, and the size. It then sleeps for 1000 milliseconds, takes another snapshot, and reports the difference.

Snapshot differenceEvent
Path in the new snapshot but not the old oneCreated
Path in both, with a changed modification time or sizeModified
Path in the old snapshot but not the new oneDeleted

The baseline snapshot is taken when the command starts, so changes made before you started watching are never reported. The first possible event arrives at least one second after launch.

Renames appear as a delete plus a create. A snapshot diff carries no file identity, so it cannot correlate the two ends of a rename. A moved or renamed file surfaces as a Deleted event for the old path and a Created event for the new one. To detect renames, correlate a Deleted and a Created in the same cycle yourself.

Only synced files are watched. Two filters decide what counts, and both are silent about what they drop. The walk skips any path segment starting with a dot, so nothing under .jotura/ or .md_store/ ever appears. It then keeps only files whose lowercased extension is on a fixed allowlist.

GroupExtensions
Textmd, markdown, mdc, txt, rst, org, tex, json, yaml, yml, toml, csv, tsv, xml, html, htm, css, scss, sass, js, mjs, ts, tsx, jsx, py, rb, rs, go, java, kt, swift, c, cpp, h, hpp, sh, bash, zsh, fish, ini, conf, env, sql, log, ps1, bat
Binarypdf, docx, doc, xlsx, xls, pptx, ppt, odt, ods, odp, rtf, epub, png, jpg, jpeg, gif, webp, svg, bmp, ico, heic, heif, avif, tiff, tif

Only the last extension segment counts, so note.tar.md is watched and note.tar.gz is not. A file with no extension at all, such as README, is never watched. Dropping a .zip, an .mp4, or an extensionless file into the vault produces no event and no explanation.

A change that preserves both modification time and size is invisible. If a tool rewrites a file to the same byte length and restores the original timestamp, the diff sees nothing and no event is emitted.

A read failure ends the watcher. Each poll re-walks the vault, and an error there propagates out of the command. If the vault directory becomes unreadable while the watcher is running, the process exits 1 with the usual JSON error on stderr. A long-running watcher needs supervision, not just a pipe.

Stopping the watcher

Press Ctrl-C. The handler sets a stop flag, which the loop checks immediately before and immediately after each sleep. A Ctrl-C arriving early in a cycle therefore waits out the rest of the 1000 millisecond sleep, so shutdown takes up to about a second. The process then exits 0. No partial line is left on stdout, because each event is written as one complete line.

Handler registration is best-effort. If something else in the process had already installed a signal handler, watch carries on without its own, and Ctrl-C falls back to the default termination.

Reporting vault state

The status command

jotura status reports the resolved vault path, the note count, and a brief view of sync.

jotura status

It always emits JSON and ignores --json, so there is no text form to parse.

{ "vault": "/Users/you/Notes", "noteCount": 1234,
  "sync": { "enabled": true, "keyCached": true, "paused": false },
  "shares": { "count": 2 } }

noteCount is not a count of notes. It counts every synced file, using exactly the walk described under the polling model above. Every allowlisted file contributes, so PDFs, images, Word documents, and any source files sitting in the vault are all included alongside your Markdown. Dot-prefixed paths are excluded, which is why .md_store/ mirrors and .jotura/templates/ do not contribute. Files with an extension off the allowlist are excluded too. Expect this number to disagree with a plain ls count and with the desktop app’s note count.

The sync object here is the brief form with three fields. If you need the pending operation count or per-share detail, use jotura sync status instead, which is documented in Sync and shares. Sync is the paid part of Jotura, so on a vault that has never had sync turned on these fields simply report enabled: false. See pricing for what a subscription covers.

shares.count is the number of entries in the share registry. A registry that fails to parse is treated as empty for reads, so a broken registry reports count: 0 on a vault that really has shares. That case is not silent: the CLI prints a warning to stderr once per invocation, naming the registry path and the parse error. If you see a zero you did not expect, run jotura doctor and read its shares-registry check, which reports the same condition in a form you can parse.

The vault info command

jotura vault info reports the same information as status plus the total size on disk.

jotura vault info

It also always emits JSON and ignores --json.

{ "vault": "/Users/you/Notes", "noteCount": 1234, "totalSize": 5678901,
  "sync": { "enabled": true, "keyCached": true, "paused": false },
  "shares": { "count": 2 } }

totalSize is the summed byte size of the same synced files that noteCount counts. Every caveat about what that set includes and excludes applies here too.

One wrinkle in the help text: the parent vault command describes itself as reporting “paths, KDF params, totals”, but no key derivation parameters are emitted. That phrase is left over from the era when vaults were encrypted on disk. vault info returns only the five fields shown above.

Both status and vault info need a vault. If none can be resolved from --vault, $JOTURA_VAULT, or the desktop app’s last opened vault, they exit 6.

Running the health checks

jotura doctor runs a fixed sequence of checks and reports each one as a pass, warning, failure, or informational line.

jotura doctor

doctor takes no command-specific arguments and it respects --json. It is also one of the few commands that works with no vault at all. It resolves the vault itself and reports a failure to do so as a check rather than exiting 6.

Output shapes

The text form prints a header, then one line per check with the name padded to 24 characters, then a trailing line when anything failed.

jotura doctor:
  [ok  ] cli-on-path              /usr/local/bin/jotura (on $PATH)
  [info] cli-version              0.7.0
  [ok  ] vault-detected           /Users/you/Notes

When at least one check has status fail, a blank line and one fixed sentence follow the list:

one or more checks failed; see details above

The JSON form is an array of objects with three fields each.

jotura doctor --json
[ { "name": "cli-on-path", "status": "pass", "detail": "/usr/local/bin/jotura (on $PATH)" } ]

status is one of pass, warn, fail, or info. In the text form these render as ok , warn, fail, and info respectively.

doctor always exits 0, even when a check fails. You cannot use its exit code as a health gate. A script that wants to act on the result must parse the JSON and look at the status fields itself.

jotura doctor --json | jq -e 'map(select(.status == "fail")) | length == 0'

The checks

Checks are emitted in this order. The array length varies with your vault, and so does the set of names present, so parse by name rather than by position.

NameStatus valuesWhat it reports
cli-on-pathpass, warnThe path of the running executable and whether it is reachable from $PATH.
cli-versioninfoThe version of the running binary.
vault-detectedpass, failThe resolved vault path, or advice to set $JOTURA_VAULT, pass --vault, or open a vault in the desktop app.
vault-readablepass, failWhether the vault directory lists cleanly.
sync-enabledinfo, failEither enabled with active or paused state and a pending operation count, or not enabled (.jotura/sync.db missing). A read error on the sync state is a fail.
sync-key-cachedpass, warnOnly emitted when sync is enabled. A warning tells you to run jotura sync login.
shares-registryinfo, warnWhether the share registry parses, and how many entries it holds. A warning means reads treat the registry as empty and writes are refused until it is repaired. When it warns, the per-share checks below are skipped.
share-mountpass, failOne per share. A missing mount is a failure, and the detail warns that leaving a stale entry in place can propagate a share-wide deletion the next time the desktop app launches.
share-sync-dbinfoOne per share. Reports that the share sync database is missing, or that it exists with a pending operation count, or that it exists but could not be read, with the underlying error.
share-key-cachedinfoOne per share: whether a share key is cached.
documentsinfo, warnConversion totals in the form N converted / N failed / N pending / N total. Any failures make this a warning.
documents-failedwarnOne check per failing document path, up to ten. Only present when the documents check counted at least one failure.
documents-failed-noteinfoPoints you at jotura regenerate-md-store --stale-only. Present under the same condition as documents-failed.
semantic-indexinfo, warn, passMissing, disabled, unopenable, or enabled with a chunk count and pending count.
semantic-modelpass, warnThe embedding model directory, or a warning. There are two warnings: no config directory could be resolved for the model, or the directory resolved but the model files are missing.
semantic-runtimepass, warnloadable with the active backend, or the reason the engine failed to load.
desktop-app-versionpass, infoThe appVersion recorded in the desktop app’s settings.json. Three separate info details cover the cases where that is unavailable: no resolvable config directory on this platform, no settings.json found, or a settings.json with no version stamped yet. The middle case is normal on a machine that only has the CLI.
markitdown-sidecarpass, warnThe converter sidecar version, or one of two warnings: not found next to the CLI, so document conversion produces stub bodies, or found but its version probe returned nothing usable.
markitdown-sandboxpass, warn, fail, infoHow the document converter is sandboxed. See the next section.
diagnostics-loginfoThe path to the local diagnostics log, or a note that no config directory is resolvable on this platform. In that second case diagnostics-recent never appears.
diagnostics-recentinfoOne check per line for the five most recent diagnostics log lines, only when the log exists.

Several checks can report fail, verified against the current source: vault-detected, vault-readable, sync-enabled when the sync state cannot be read, share-mount for a missing mount, and markitdown-sandbox when no usable sandbox is available. None of them changes the exit code.

The documents block has one more way to disappear. If the conversion summary cannot be read at all, the documents, documents-failed, and documents-failed-note checks are all absent, and no check reports why. Absent is not the same as failing, and a consumer looking up documents by name has to handle a missing key.

The semantic checks are deliberately informational about availability. Keyword search works without any of them, so a missing index or model never blocks reading and searching. Semantic indexing itself is a desktop feature and the CLI is query-only, which is covered in Reading and searching.

What runs when no vault resolves

doctor being vault-free is useful, but it cuts the report down sharply. Everything from vault-readable through semantic-runtime runs only when a vault was resolved. With no vault you get cli-on-path, cli-version, vault-detected with status fail, desktop-app-version, markitdown-sidecar, markitdown-sandbox, and the diagnostics-log line.

Every sync, share, document, and semantic check is absent in that case. They are not failing and not warning, so an empty result for one of those names means the vault never resolved, not that the subsystem is healthy.

Reading the sandbox check

markitdown-sandbox is the check most likely to surprise you, because it is the one that can block a feature outright. The document converter processes untrusted files, so Jotura sandboxes it. A machine with no usable sandbox refuses to convert rather than running the converter unprotected.

SituationStatusDetail
No converter installedinfoNothing to sandbox.
sandbox-exec on macOSpassConverter sandboxed, network denied.
bubblewrap on LinuxpassConverter sandboxed, network denied.
Restricted token and job object on WindowspassConverter sandboxed, but the detail notes that network access is not denied.
Running unsandboxed by explicit opt-inwarnNames the opt-in that is active.
No usable sandbox availablefailDocument conversion is blocked, with the reason and advice to install bubblewrap or enable unsandboxed conversion in the desktop settings.

Two environment variables turn the sandbox off deliberately, and doctor reports either one as a warning so an opt-in is never silent.

VariableEffect
JOTURA_DISABLE_SANDBOXRuns the document converter unsandboxed.
JOTURA_ALLOW_UNSANDBOXED_CONVERSIONThe same effect through a differently named opt-in.

The desktop app has an equivalent setting, allowUnsandboxedConversion, and the warning detail names whichever route is in effect. Document conversion from the CLI is covered in Documents from the CLI.

Printing the conventions note

jotura conventions prints the vault’s Agent Conventions note, creating it from a bundled seed the first time you run it.

jotura conventions

The note lives at the fixed vault path Agent Conventions.md, at the vault root, with a space in the filename. It is an ordinary vault note in every respect. The desktop app shows it, sync carries it end-to-end encrypted along with everything else, every agent runtime that reads the vault shares it, and no installer ever touches it.

The first run is a write, which has three consequences. A filesystem permission failure exits 8, and any other write failure exits 1. The new note appears in a concurrently running jotura watch as a Created event. And the created field in the JSON output is true on that run only.

The command respects --json.

jotura conventions --json
{ "path": "Agent Conventions.md",
  "hash": "9f2c1ab4e0d75b3a",
  "body": "# Agent Conventions\n\n## Structure\n",
  "created": false }
FieldMeaning
pathAlways Agent Conventions.md.
hashThe current content hash, suitable for passing to edit --if-hash.
bodyThe note body with the YAML frontmatter stripped. There is no separate frontmatter field here, which is a difference from read.
createdtrue only on the run that created the note from the seed.

The text form prints the whole file including frontmatter, so the two forms do not carry the same content.

conventions is the read half of the loop. To change a convention, edit the note like any other note with the commands in Editing notes, using the hash from the JSON output as the precondition.

HASH=$(jotura conventions --json | jq -r .hash)
jotura edit "Agent Conventions.md" --append --content "- Meeting notes live under Meetings/." --if-hash "$HASH"

--append already guarantees a newline between the old body and the new block, and a trailing newline after it, so the content needs no newlines of its own.

The wider agent configuration surface, including the vault’s Agents/ folder and jotura agents sync, is documented in The agent surface.

Exit codes on this page

CodeSymbolWhen you will see it here
0-Everything on this page that completes, including doctor runs with failing checks and a watch stopped with Ctrl-C.
1ErrorA vault read failure during a watch poll, or a non-permission write failure on the conventions create path.
2HashConflictOnly from the edit in the conventions example above, when the note changed between the read and the write.
6NoVaultstatus, vault info, watch, and conventions with no resolvable vault. doctor reports this as the vault-detected check instead.
8PermissionDeniedA filesystem permission failure on the conventions create path.
9InvalidArgswatch --paths with an invalid glob, or watch --kinds with R, RENAMED, or any token other than C, M, and D.

One caveat applies to every command. Clap, the argument parser, uses exit code 2 for its own usage errors, such as a missing required argument or an unknown flag. That collides with HashConflict. Usage errors print human-readable text on stderr rather than the JSON error envelope, so a caller that sees exit 2 should check whether stderr parses as JSON before concluding it hit a hash conflict. The full code list is in Exit codes and JSON output.