Jotura

Docs / The CLI / Sync and shares

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 split is intentional. The desktop app holds the sync loops, the password ceremony, and share membership. The CLI holds local state and reporting.

TaskWhere it happens
Enabling sync on a vault and choosing the sync passwordDesktop app
Changing or rotating the sync passwordDesktop app
Showing or regenerating the 12-word recovery phraseDesktop app
Restoring a vault from the server onto a new machineDesktop app
Running the actual push and pull loopsDesktop app
Creating a share, inviting members, changing roles, unsharing, leavingDesktop app
Mounting a share you have just been givenDesktop or mobile app
Building the semantic search indexDesktop 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.

FlagTypeDefaultWhat it does
--passphrase-stdinbooleanfalseRead 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-phrasebooleanfalseUnlock 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 answer different questions, so passing both is meaningful rather than contradictory. --passphrase-stdin decides how the secret is read, and --recovery-phrase decides how the secret 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" }

Two caveats sit with this command. It deletes the cache file, but it does not shred the key from the memory of processes that are already running. An open desktop app keeps syncing until you sign out there too.

The second caveat is scope. Logout forgets the personal vault key only. Keys for individual shares live under separate cache entries that the CLI never writes and never deletes.

Logging out does not affect your local notes at all. Sync simply 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.

PropertyValue
Location<config dir>/jotura/sessions/
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%
Vault key file nameThe lowercase hex SHA-256 of the canonical vault path, one file per vault
Share key file nameThe lowercase hex SHA-256 of the literal string share:<share id>, one file per share
File contentsBase64 of the 32-byte key
Write methodWritten to a temporary sibling, flushed, then renamed into place
Unix permissionsMode 0600 on the file, inside a directory created with mode 0700
Windows permissionsInherited from the user profile directory’s access control list

Because this is an ordinary file and not an operating system keychain entry, nothing in the login flow raises a keychain dialog and nothing prompts for your login password. That is deliberate. An agent or a script running jotura sync login must never be blocked by a graphical prompt it cannot answer.

The cache is checked, not trusted blindly. When a command reads the cached key it verifies it against the verifier stored in the vault’s sync database. A key that fails verification, or a cache file that is malformed, is deleted and reported as not cached rather than being used. So keyCached: false after a password change is expected, and the fix is to run jotura sync login again.

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.

CommandBehavior on a vault with no sync
jotura sync statusExits 0 and reports enabled: false
jotura sync loginExits 12
jotura sync logoutExits 12
jotura sync drop-pathExits 12
jotura status, jotura vault infoExit 0, with sync.enabled set to false
Every data commandUnaffected, because plain files need no key

The fix is always the same. 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"

Note that jotura sync --help does not list this subcommand. Its summary line reads “Sync key management (status, login, logout)”, which is stale. The command exists, and jotura sync drop-path --help documents it.

ArgumentTypeDefaultWhat it does
<PATH>stringrequiredThe 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:

CodeSymbolConditionMessage
3NotFoundNo synced row matches the string exactly.<path> is not a synced path (exact string match; drop-path never touches files)
9InvalidArgsThe path is empty, or is .jotura or anything under it.refusing to drop internal or empty paths
12SyncNotEnabledThe vault has no .jotura/sync.db.Standard SyncNotEnabled message.

One ordering detail matters when you script against this command. It validates the path argument before it checks whether sync is enabled. An empty or internal path therefore exits 9 even on a vault that would otherwise have exited 12.

This is maintenance tooling, not a delete command. Its intended use is 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 instead would propagate a genuine deletion of your notes to every device. 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.

ColumnValuesMeaning
NAMEfree textThe share’s name as set by its owner.
MOUNTvault-relative pathWhere the share is mounted in this vault. Received shares mount under Shared with me/<name>.
KINDfolder, fileWhether a whole folder or a single note is shared.
ROLEeditor, viewerYour role in this share. viewer means read-only.
OWNERSHIPowned, receivedWhether you created the share or were given it.
MOUNTEDyes, MISSINGWhether the mount path actually exists on disk.
SYNCDByes, noWhether this share has local sync state yet.
PENDINGintegerOperations queued for this share.
PAUSEDyes, noWhether this share’s sync is paused.
KEYcached, absentWhether 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
  }
]

MOUNTED showing MISSING in uppercase is a warning, not a formatting quirk. 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

The CLI does not confine itself to a shares namespace. Once a share is mounted, several commands surface it.

CommandWhat it adds
jotura read <path> --jsonA share object with shareId, name, and role, present only when the path routes into a share mount.
jotura ls --jsonshareId and shareRole on each entry inside a mount. These fields appear in JSON mode only.
jotura statusA shares object carrying a count.
jotura vault infoThe same shares count alongside vault totals.
jotura sync statusThe per-share array documented above.
jotura doctorPer-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:

CommandGuarded target
jotura writeThe destination path
jotura editThe note being edited
jotura createThe note being created
jotura frontmatter set, jotura frontmatter deleteThe note being modified
jotura tag add, jotura tag removeThe note being modified
jotura todayThe daily note, which the bare form creates when it does not yet exist, as do --read and --append
jotura importThe import destination, covered in Documents from the CLI
jotura trash <path>The note being trashed
jotura trash restoreThe restore destination, original or renamed
jotura deleteThe note or folder being deleted
jotura renameThe destination always, plus the source when the rename is not a mount relocation
jotura batchEvery 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",
  "path": "Shared with me/Team notes/roadmap.md",
  "shareId": "01HQ8ZK3M7VWXY2N4PR6STB9CD",
  "mountRel": "Shared with me/Team notes"
}

There is deliberately no override flag. A forced write into a viewer mount would never sync, and the next remote change would overwrite it, so the write would only look like it worked. The way forward is to ask the owner for editor access in the desktop app, or to copy the content to a path you own.

Note that this is a guard against silent data loss, not a security boundary. A viewer can read the files, so a viewer can copy them. The desktop app states the same thing 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"

So a clean dry run is not proof that 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 actionResult
jotura delete or jotura trash on a mount rootExit 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 mountExit 9, naming the mount it contains
jotura rename to a destination that is a mount root or contains oneExit 9: <path> is a shared mount or contains one; choose a different destination

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.

One gap is worth naming plainly. jotura trash purge routes through no share guard at all, unlike jotura trash <path>, jotura trash restore, and jotura delete. It skips the viewer guard and the mount-structure guards alike. 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 does not sync, but it does keep the queue honest. 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, and the difference is worth knowing before you run it. 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:

RenameWhat is enqueued
A share mount root, or a folder containing oneNothing. 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 changedA 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 boundaryA 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 nameStatusesWhat it reports
sync-enabledinfo, failinfo 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-cachedpass, warnEmitted only when sync is enabled. The warning advises running jotura sync login.
shares-registryinfo, warnWhether 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-mountpass, failOne 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-dbinfoOne per share: whether local sync state exists, and its pending operation count.
share-key-cachedinfoOne 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. Scripts cannot use its exit code as a health gate and must parse the statuses. The full check list is in Watching and diagnostics.

Exit codes used on this page

CodeSymbolWhen you see it here
0successIncluding sync status on a vault with no sync, and any dry run in a viewer mount.
1generic errorAn unreadable share registry on a write path.
3NotFoundsync drop-path given a path with no matching synced row.
6NoVaultNo vault resolved, so there was nothing to report on.
9InvalidArgsWrong 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.
12SyncNotEnabledsync login, sync logout, or sync drop-path on a vault with no sync database.
15ReadOnlyShareAny 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.