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.
| 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 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.
| Property | Value |
|---|---|
| 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 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 |
| 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 permissions | Inherited 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.
| 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 |
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.
| 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. |
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.
| 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 actually 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
}
]
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.
| 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",
"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 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 |
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:
| 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. 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
| 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.
Related pages
- Install and setup covers vault resolution, the global flags, and getting the binary onto your path.
- Editing notes covers the hash-checked write loop that these guards sit in front of.
- Creating, moving, and deleting covers
rename,delete, and the trash commands in full. - Watching and diagnostics covers
jotura doctorin full. - Sync and sharing covers the desktop side: enabling sync, the recovery phrase, share management, and restore.
- Security explains what the server can and cannot see.