Sync and shares

How the jotura CLI reports sync state, caches the sync key, lists shares, and refuses writes into view-only share mounts, plus the exit codes involved.

Jotura’s cloud sync is end-to-end encrypted, and the desktop app owns it. The command-line interface has a narrower job. It reports sync state, manages the local cache of the sync key, lists the shares mounted in your vault, and refuses writes that could never reach the other people in a share.

None of this gates your notes. A vault is a plain folder of Markdown files, so reading, searching, and editing work whether or not sync is configured, whether or not the key is cached, and whether or not you are online. There is no unlock step before data commands. See Install and setup for how the CLI picks a vault.

Enabling sync and creating shares happen in the desktop app and require a paid Sync subscription, described on the pricing page.

What the CLI deliberately does not do

The desktop app holds the sync loops, the password setup, and share membership, while the CLI handles local state and reporting.

Task Where it happens
Enabling sync on a vault and choosing the sync password Desktop app
Changing or rotating the sync password Desktop app
Showing or regenerating the 12-word recovery phrase Desktop app
Restoring a vault from the server onto a new machine Desktop app
Running the actual push and pull loops Desktop app
Creating a share, inviting members, changing roles, unsharing, leaving Desktop app
Mounting a share you have just been given Desktop or mobile app
Building the semantic search index Desktop app

Two consequences follow from that table. There are no push or pull commands in the CLI, so anything the CLI enqueues transmits when the desktop app next syncs. And a machine that only ever runs the CLI cannot receive a new share, because discovery and mounting happen in the desktop or mobile app. Once a share is mounted, the CLI is fully aware of it.

For the desktop side of all of this, see Sync and sharing.

jotura sync status

Report whether sync is enabled for this vault, whether the key is cached, whether sync is paused, how many operations are pending, and the same facts for each mounted share.

jotura sync status

This command never fails because sync is missing. A vault with no sync database reports enabled: false rather than exiting 12. It takes no command-specific flags and respects the global --json flag.

Text output for a vault with sync enabled:

sync: enabled, key cached, active, 3 pending op(s)

The key field reads key cached or key not cached. The pause field reads active or paused. A vault with no sync prints sync: not enabled instead of that line.

One line follows per mounted share, in the form share <name>: <role>, <active|paused>, N pending op(s):

share Team notes: editor, active, 0 pending op(s)

The share lines print even when personal sync was never set up on this vault, because a share carries its own sync state.

JSON output:

{
  "enabled": true,
  "keyCached": true,
  "paused": false,
  "pendingOps": 3,
  "shares": [
    {
      "shareId": "01HQ8ZK3M7VWXY2N4PR6STB9CD",
      "name": "Team notes",
      "role": "editor",
      "pendingOps": 0,
      "paused": false
    }
  ]
}

Share identifiers are minted by the server as 26 uppercase characters from the alphabet 0123456789ABCDEFGHJKMNPQRSTVWXYZ, with no prefix and no separator. Match them as opaque strings rather than parsing them.

jotura status reports a shorter version of the same thing, carrying only enabled, keyCached, and paused plus a share count. Use sync status when you need pendingOps or the per-share breakdown.

jotura sync login

Unlock the vault’s sync key and write it to the local key cache so later sessions, and the desktop app, do not have to ask again.

jotura sync login

With no flags, the command prompts on the terminal with sync password: and reads the password without echoing it.

Flag Type Default What it does
--passphrase-stdin boolean false Read the secret from standard input instead of prompting. The whole stream is read to end of file and trailing carriage returns and newlines are stripped.
--recovery-phrase boolean false Unlock with the 12-word BIP39 recovery phrase instead of the sync password. Without --passphrase-stdin this prompts with recovery phrase: , again without echo.

The two flags control separate things and can be combined: --passphrase-stdin decides how the secret is read, and --recovery-phrase decides how it is used. Passing both reads the recovery phrase from standard input.

# Prompt for the sync password interactively.
jotura sync login

# Feed the password from a secret manager, with no prompt and no echo.
pass show jotura/sync | jotura sync login --passphrase-stdin

# Unlock with the recovery phrase, prompted interactively.
jotura sync login --recovery-phrase

On success the command always prints JSON, whether or not you pass --json:

{ "status": "keyCached", "vault": "/Users/you/Vault" }

A wrong password and an invalid recovery phrase both exit 9 (InvalidArgs), not a dedicated authentication code. The message field tells them apart, and both strings are fixed: incorrect sync password and invalid recovery phrase. A vault with no sync database exits 12 instead.

jotura sync logout

Forget the cached sync key for this vault.

jotura sync logout

There are no command-specific flags. Success always prints JSON, whether or not you pass --json:

{ "status": "keyForgotten", "vault": "/Users/you/Vault" }

Logout deletes the cache file, but processes that are already running keep the key in memory, so an open desktop app keeps syncing until you sign out there too.

Logout also forgets the personal vault key only. Keys for individual shares live under separate cache entries that the CLI never writes or deletes.

Your local notes are unaffected by logout. Sync pauses until the key is cached again.

The key cache model

Both the desktop app and the CLI read and write the same cache, which is why unlocking in one surface resumes the other. The trust boundary is anything running as the current operating system user, the same boundary as ~/.ssh or a shell’s credential helper.

Property Value
Location <config dir>/jotura/sessions/ on macOS and Linux, %LOCALAPPDATA%\jotura\sessions\ on Windows
Config dir on macOS ~/Library/Application Support
Config dir on Linux $XDG_CONFIG_HOME, or ~/.config when that is unset
Config dir on Windows %APPDATA%, used for the cache only with JOTURA_SECRET_STORE=plain
Vault key file name The lowercase hex SHA-256 of the canonical vault path, one file per vault
Share key file name The lowercase hex SHA-256 of the literal string share:<share id>, one file per share
File contents Base64 of the 32-byte key. A share file holds every key generation of that share, oldest first
Write method Written to a temporary sibling, flushed, then renamed into place
Unix permissions Mode 0600 on the file, inside a directory created with mode 0700
Windows protection Each file is encrypted with your Windows account (DPAPI). A session that cannot use DPAPI, such as a key-based SSH login, reads an older plaintext copy from %APPDATA%\jotura\sessions\ when one exists and never deletes it. JOTURA_SECRET_STORE=plain keeps plaintext files in %APPDATA%\jotura\sessions\ for a machine where SSH and the desktop must share the cache

The cache is an ordinary file rather than an operating system keychain entry, so the login flow never raises a keychain dialog or prompts for your login password. This is by design, because an agent or a script running jotura sync login has no way to answer a graphical prompt.

When a command reads the cached key, it checks it against the verifier stored in the vault’s sync database. A key that fails verification, or a malformed cache file, is deleted and reported as not cached. Expect keyCached: false after a password change, and run jotura sync login again to fix it.

Share keys sit in the same directory under their own entries, with the same permissions. The CLI reads those entries to report the KEY column in jotura shares list, but it never creates or clears them. The desktop app is what populates a share key.

JOTURA_SESSION_DIR overrides the sessions directory entirely. It exists so the test suite can point at a temporary directory instead of the real config directory. Leave it unset in normal use.

Exit code 12 when sync is not enabled

Exit code 12 is SyncNotEnabled. It means the vault has no .jotura/sync.db, so there is no key to unlock, forget, or enqueue against.

Command Behavior on a vault with no sync
jotura sync status Exits 0 and reports enabled: false
jotura sync login Exits 12
jotura sync logout Exits 12
jotura sync drop-path Exits 12
jotura status, jotura vault info Exit 0, with sync.enabled set to false
Every data command Unaffected, because plain files need no key

To fix it, enable sync for that vault in the desktop app, then run jotura sync login on this machine.

jotura sync drop-path

Remove a path from sync tracking and propagate that deletion to your other devices. This command never touches local files.

jotura sync drop-path "daily/2026/08/x.md"

jotura sync --help lists this subcommand, but the summary line for sync still reads “Sync key management (status, login, logout)”, which is stale. jotura sync drop-path --help documents it.

Argument Type Default What it does
<PATH> string required The synced path string to drop. This is an exact string match against the synced-paths table after normalization, not a filesystem lookup.

It respects the global --json flag. Text output is a single line:

enqueued sync delete for daily/2026/08/x.md (no local file touched)

JSON output:

{
  "path": "daily/2026/08/x.md",
  "isFolder": false,
  "enqueued": true,
  "note": "deletion propagates on the app's next sync; no local file was touched"
}

Exit codes specific to this command:

Code Symbol Condition Message
3 NotFound No synced row matches the string exactly. <path> is not a synced path (exact string match; drop-path never touches files)
9 InvalidArgs The path is empty, or is .jotura or anything under it. refusing to drop internal or empty paths
12 SyncNotEnabled The vault has no .jotura/sync.db. Standard SyncNotEnabled message.

If you script against this command, note that it validates the path argument before it checks whether sync is enabled, so an empty or internal path exits 9 even on a vault that would otherwise have exited 12.

drop-path is maintenance tooling for repairing orphaned or case-aliased path strings, for example a daily/ row left behind when the real folder on disk is Daily/. Fixing a case alias by deleting the file would propagate a real deletion of your notes to every device, so use drop-path for the tracking row and leave the file alone.

The enqueued operation transmits when the desktop app next syncs. The CLI does not transmit it.

jotura shares list

List every share mounted in this vault with its mount point, role, and sync state.

jotura shares list

There are no command-specific flags. It respects the global --json flag. An empty registry prints no shares.

Text output is a padded table with a header row and one row per share:

NAME        MOUNT                      KIND    ROLE    OWNERSHIP  MOUNTED  SYNCDB  PENDING  PAUSED  KEY
Team notes  Shared with me/Team notes  folder  editor  received   yes      yes     0        no      cached
Recipes     Recipes                    folder  editor  owned      yes      yes     2        no      cached

Column widths grow to fit the longest value, so the output is wide. Pipe it through less -S if your terminal wraps it.

Column Values Meaning
NAME free text The share’s name as set by its owner.
MOUNT vault-relative path Where the share is mounted in this vault. Received shares mount under Shared with me/<name>.
KIND folder, file Whether a whole folder or a single note is shared.
ROLE editor, viewer Your role in this share. viewer means read-only.
OWNERSHIP owned, received Whether you created the share or were given it.
MOUNTED yes, MISSING Whether the mount path exists on disk.
SYNCDB yes, no Whether this share has local sync state yet.
PENDING integer Operations queued for this share.
PAUSED yes, no Whether this share’s sync is paused.
KEY cached, absent Whether the share’s key is in the local key cache.

JSON output is an array of objects with the same information in camelCase:

[
  {
    "shareId": "01HQ8ZK3M7VWXY2N4PR6STB9CD",
    "name": "Team notes",
    "mountRel": "Shared with me/Team notes",
    "kind": "folder",
    "role": "editor",
    "isOwner": false,
    "mountExists": true,
    "syncDbExists": true,
    "pendingOps": 0,
    "paused": false,
    "keyCached": true
  }
]

MISSING is printed in uppercase on purpose, as a warning. It means the registry points at a mount that is not on disk, which is a stale entry worth repairing in the desktop app before the next launch reconciles it.

The share registry lives at <vault>/.jotura/shares/registry.json, and each share’s sync state lives at <vault>/.jotura/shares/<share id>/sync.db. If the registry cannot be parsed, every command that resolves a vault prints a warning to standard error naming the registry path. Reads then carry on as though there were no shares. Writes refuse with exit 1 rather than risk writing through a guard that is not working.

Share awareness in other commands

Once a share is mounted, several commands outside the shares namespace surface it.

Command What it adds
jotura read <path> --json A share object with shareId, name, and role, present only when the path routes into a share mount.
jotura ls --json shareId and shareRole on each entry inside a mount. These fields appear in JSON mode only.
jotura status A shares object carrying a count.
jotura vault info The same shares count alongside vault totals.
jotura sync status The per-share array documented above.
jotura doctor Per-share checks, described below.

A quick way to see the role for a path before you try to edit it:

jotura read "Shared with me/Team notes/roadmap.md" --json | jq '.share'

Reading and listing are covered in Reading and searching.

Viewer mounts and exit code 15

Exit code 15 is ReadOnlyShare. It means the target path is inside a share where your role is viewer, so you can read the note but cannot save changes to it.

The guard lives in four entry points that every mutation passes through: a write guard, a remove guard, a rename guard, and a batch pre-flight pass that applies the other three to every entry before any write happens. The commands they cover:

Command Guarded target
jotura write The destination path
jotura edit The note being edited
jotura create The note being created
jotura frontmatter set, jotura frontmatter delete The note being modified
jotura tag add, jotura tag remove The note being modified
jotura today The daily note, which the bare form creates when it does not yet exist, as do --read and --append
jotura import The import destination, covered in Documents from the CLI
jotura trash <path> The note being trashed
jotura trash restore The restore destination, original or renamed
jotura delete The note or folder being deleted
jotura rename The destination always, plus the source when the rename is not a mount relocation
jotura batch Every entry, checked before any write happens

The rename row is split for a reason. When the source is a share mount root, or a folder containing one, only the destination is checked, because that rename is a local relocation rather than a write into the share. The mount-structure section below covers what that means.

The error is JSON on standard error, as all CLI errors are:

{
  "code": "ReadOnlyShare",
  "message": "Shared with me/Team notes/roadmap.md is in a read-only shared mount (Shared with me/Team notes); ask the share owner for editor access. Paths are matched without regard to case.",
  "path": "Shared with me/Team notes/roadmap.md",
  "shareId": "01HQ8ZK3M7VWXY2N4PR6STB9CD",
  "mountRel": "Shared with me/Team notes"
}

There is no override flag, by design. A forced write into a viewer mount would never sync, and the next remote change would overwrite it, so the write would only appear to succeed. Ask the owner for editor access in the desktop app, or copy the content to a path you own.

The guard exists to prevent silent data loss. It gives no protection against copying, since anyone who can read the files can copy them, and the desktop app says the same in its sharing copy.

Dry runs do not reach the guard

Every --dry-run branch returns before the share guard runs. This affects write, edit, frontmatter set, frontmatter delete, tag add, tag remove, create, rename, delete, delete --trash, and batch, all of which reach their dry-run output without touching the guard.

# Prints a successful dry-run envelope and exits 0, even in a viewer mount.
jotura write "Shared with me/Team notes/x.md" --content "hi" --dry-run

# The same write for real exits 15.
jotura write "Shared with me/Team notes/x.md" --content "hi"

A clean dry run therefore says nothing about whether the real run will be permitted. If you are gating a script on permission, check the role first with jotura read <path> --json and look at .share.role.

Mount structure guards and exit code 9

Separately from the viewer rule, the CLI protects the shape of a share mount. These refusals exit 9 (InvalidArgs) and apply regardless of your role, because leaving or unsharing is a membership action rather than a file action.

Attempted action Result
jotura delete or jotura trash on a mount root Exit 9: <path> is a shared mount; leave or unshare it in the desktop app's share management instead
jotura delete or jotura trash on a folder that contains a mount Exit 9, naming the mount it contains
jotura rename to a destination that is a mount root or contains one Exit 9: <path> is a shared mount or contains one; choose a different destination
jotura rename that moves the Jotura/ settings folder, or anything in it, into a share Exit 9: the Jotura settings folder cannot move into a share
jotura rename that moves a mount into the Jotura/ settings folder Exit 9: a share cannot move into the Jotura settings folder

Renaming a mount root, or a folder that contains one, is allowed. It is treated as a local relocation rather than a change to the share, so no content moves relative to the share, and the registry is rewritten to follow the new path. That works for viewers as well as editors, because nothing is being pushed.

jotura trash purge is a known gap: unlike jotura trash <path>, jotura trash restore, and jotura delete, it routes through no share guard, skipping both the viewer guard and the mount-structure guards. Whether that is intentional has not been verified. Items reaching purge are already out of the live tree.

What the CLI enqueues after a write

The CLI never transmits anything, but it keeps the sync queue current. After a successful mutation it routes the changed path to the owning sync unit, either the personal vault database or a specific share’s database, and enqueues the corresponding operation. This mirrors what the desktop app’s file watcher does, so a note you write from the CLI is not invisible to sync until the app happens to notice it.

Five conditions make the router enqueue nothing at all:

  • A sync database that does not exist is skipped, never created. Writing to a vault with no sync configured enqueues nothing.
  • Paths with a dot-prefixed component are skipped, which is why .jotura/ and .md_store/ changes never enter the queue.
  • A path that routes into a share where your role cannot push enqueues nothing. That is independent of the exit-15 guard, and it matters because relocating renames are allowed for viewers.
  • A path that is neither a directory nor a recognized synced file type enqueues nothing.
  • A share whose local sync database has not been created yet enqueues nothing for that share.

Trashing a note in a share behaves differently from trashing a private note. Your copy moves into the vault trash, which is local only, and a delete operation is enqueued for the share. Other members see the note deleted. Restoring from your trash later enqueues a put, which brings it back for everyone.

Renames have three distinct behaviors, depending on what is being renamed:

Rename What is enqueued
A share mount root, or a folder containing one Nothing. The registry’s mount path is rewritten and the share’s contents have not moved relative to the share.
A single-note share where you are an editor and the filename changed A delete of the old name and a put of the new one, in the share’s database, so other members converge on the new name.
A path that crosses a mount boundary A delete in the source unit and a put in the destination unit.

If enqueueing fails, the CLI prints a warning to standard error and the command still succeeds. Sync never blocks local work, and the desktop app’s reconcile pass is the backstop. The warning reads warning: sync enqueue for <path> failed (<reason>); the desktop app will reconcile. Treat it as informational unless you see it repeatedly.

Diagnosing sync and share problems

jotura doctor runs a sequence of checks and reports each one as pass, warn, fail, or info. The sync and share checks are:

Check name Statuses What it reports
sync-enabled info, fail info carries either enabled (active, N pending op(s)), enabled (paused, N pending op(s)), or not enabled (.jotura/sync.db missing). fail carries could not read sync state: <error> and means the sync state could not be read at all.
sync-key-cached pass, warn Emitted only when sync is enabled. The warning advises running jotura sync login.
shares-registry info, warn Whether the registry parses, and how many entries it holds. A warning means reads treat it as empty and writes are refused until it is repaired.
share-mount pass, fail One per share. A failure means the registry mount does not exist on disk, which if left alone can propagate a share-wide deletion at the next desktop launch.
share-sync-db info One per share: whether local sync state exists, and its pending operation count.
share-key-cached info One per share: whether a cached share key was found.
jotura doctor --json | jq '.[] | select(.name | startswith("sync") or startswith("share"))'

doctor always exits 0, even when a check reports fail, so a script that wants a health gate has to parse the statuses. The full check list is in Watching and diagnostics.

Exit codes used on this page

Code Symbol When you see it here
0 success Including sync status on a vault with no sync, and any dry run in a viewer mount.
1 generic error An unreadable share registry on a write path.
3 NotFound sync drop-path given a path with no matching synced row.
6 NoVault No vault resolved, so there was nothing to report on.
9 InvalidArgs Wrong sync password, invalid recovery phrase, a drop-path argument that is empty or internal, or a delete, trash, or rename that would break a mount’s structure.
12 SyncNotEnabled sync login, sync logout, or sync drop-path on a vault with no sync database.
15 ReadOnlyShare Any real write whose target sits inside a viewer-role share mount.

Codes 7 (Locked) and 10 (EncryptedVaultRequired) are retired. Vaults are plain files and never lock, and those numbers will not be reused. The complete table, along with the shape of the JSON error envelope, is in Exit codes and JSON output.