Docs / The CLI / Exit codes and JSON output
Exit codes and JSON output
The complete jotura exit code table, the JSON error object on stderr, the output shape of every command, and how to tell a usage error from a hash conflict.
The jotura command line tool is designed for scripted and automated use, so its exit codes and its output shapes are a stable contract. This page documents that contract: what each exit code means, what recovery looks like, what the error object on standard error contains, and what every command puts on standard output.
Two rules govern most of what follows. Errors that the CLI classifies are written as a single line of JSON on standard error, whether or not you passed --json, with one important exception covered in the section on exit code 2. Success output varies by command, and the --json flag does not control all of it.
If you have not installed the tool yet, start with Install and setup.
Global flags and the two output streams
Three flags are declared global, so the argument parser accepts them at any position. jotura --json read notes/a.md and jotura read notes/a.md --json are exactly equivalent, and both appear in the options block of every subcommand’s help.
| Flag | Effect | Stream |
|---|---|---|
--vault <PATH> | Selects the vault for this invocation, ahead of JOTURA_VAULT and the desktop app’s last opened vault. | None |
--json | Requests structured JSON on standard output. Many commands emit JSON with or without it. | stdout |
-v, --verbose | Logs progress. In practice this is one line, vault: <resolved path>, for most commands. | stderr |
The --verbose line matters more than its size suggests, because it lands on the same stream as the error object. Under -v, a failing command writes the progress line first and the JSON error second, so standard error is no longer a single JSON document. Either keep -v off in scripts that parse standard error, or parse only the last line of it. Every recipe on this page parses the last line, so it works in both cases.
Exit codes
These codes come from the CLI’s error type and are the public contract. An existing code keeps its meaning, and a retired number is never reused for something else.
| Code | Name | Meaning | Recovery |
|---|---|---|---|
| 0 | (success) | The command completed. | None needed. Note that doctor, agents status, and the hidden path completion helper return 0 even when they report problems. |
| 1 | Error | A generic, unclassified failure: an input or output error with no specific mapping, a JSON serialization failure, or a failure to read standard input. | Read the message field of the error object. |
| 2 | HashConflict | An --if-hash precondition did not match the file’s current hash, or a compare-and-set write lost a race against another writer. The same code covers a batch whose preconditions failed. | Re-run jotura read <path> --json, take the new hash, re-derive your change, and retry. See the exit 2 collision warning below. |
| 3 | NotFound | A --replace needle matched zero times, a file does not exist, a named template does not exist, or sync drop-path was given a path with no synced row. | Check spelling and letter case. The --replace needle must match literally, byte for byte. There is no regular expression matching in edit. |
| 4 | Ambiguous | A --replace needle matched more than once and neither --all nor --nth was supplied. | Add --all, add --nth N, or lengthen the needle until it is unique. The error object lists every matching line number. |
| 5 | OutOfRange | A line number or a line range fell outside the file, or --nth N was given a zero or a number larger than the occurrence count. | Re-read the file to get its current line count, then reissue the edit. |
| 6 | NoVault | No vault could be resolved, the resolved path is not readable, or it is not a directory. | Pass --vault <path>, set the JOTURA_VAULT environment variable, or open a vault in the desktop app so the CLI can inherit it. Six commands never exit 6; see below. |
| 8 | PermissionDenied | The filesystem refused the operation, or the path resolved to a location outside the vault. The message for the second case is “path resolves outside the vault”. | Fix the permissions, or correct the path. Symbolic links and Windows junctions that resolve back inside the vault are allowed and followed; ones that escape it are not. |
| 9 | InvalidArgs | A rejected path, a bad flag combination, or a value the command refused. This is a wide bucket, enumerated below the table. | Read the message, which names the specific problem. Note that a wrong sync password and a bad recovery phrase both land here rather than on a dedicated authentication code. |
| 11 | MultiEditOpFailed | One operation inside an edit --apply sequence or a batch file failed. | The error object carries opIndex so you know which one. Fix that operation and resubmit. For edit --apply nothing was written at all. For batch the guarantee is weaker; see the batch section below. |
| 12 | SyncNotEnabled | sync login, sync logout, or sync drop-path ran against a vault that has no sync database at .jotura/sync.db. | Enable sync in the desktop app first. sync status never returns this code; it reports "enabled": false instead and exits 0. |
| 13 | SemanticUnavailable | search --mode semantic or search --mode smart ran without a usable vector index. The specific cause varies: no vector database, semantic search disabled in that database, a missing embedding model, no resolvable configuration directory, or an unavailable machine learning runtime. | The message names the exact missing prerequisite. Enable semantic search in the desktop app. The CLI only queries the index and never builds it. See Reading and searching. |
| 14 | TargetMissing | An --if-hash write found that the target file had been deleted or moved since you read it. The file is deliberately not recreated. | Re-read to confirm the file is gone, then either write without --if-hash to recreate it deliberately, or abandon the change. |
| 15 | ReadOnlyShare | The target path sits inside a share you joined with the viewer role. | Ask the share owner to give you the editor role. There is deliberately no override flag, because a forced write into a viewer mount would never sync and would be overwritten by the next remote change. See Sync and shares. |
| 16 | AgentConflict | jotura agents sync found one or more files changed on both the vault side and the system side. | The conflicted files were skipped and every other file was still synced. Re-run with --prefer vault or --prefer system. See The agent surface. |
Cloud sync, which is what exit codes 12 and 15 relate to, is a paid feature; see pricing for the current cost.
What lands in exit 9
The InvalidArgs bucket is wide, so it is worth knowing what is in it. Four of its causes fire on any path argument of any command, and they are the ones an automated caller hits most often.
| Group | Causes |
|---|---|
| Path rules | An empty path after normalization, any . or .. component, and any path targeting .jotura or something under it. The one exception is the .jotura/templates/ subtree, which is addressable. A path that escapes the vault is exit 8, not exit 9. |
| Edit arguments | --replace supplied without --with, an empty --replace needle, and a line range that does not parse as N:M. |
| Command specific | write given binary content, import - without --as, regenerate-md-store with no path and neither --all nor --stale-only, templates create against a name that already exists, rename to a destination that already exists, trash with neither a path nor a subcommand, trash empty without --force, deleting or trashing a share mount root, --in combined with a semantic search mode, an invalid watch --paths glob, and an unknown watch --kinds token. |
| Value rejections | An unknown memory kind, an unknown memory importance, a title that slugifies to nothing, a malformed today --date, an unknown agent name, a wrong sync password, and an invalid recovery phrase. |
Commands that never exit 6
Six commands are handled before vault resolution runs, so they work with no vault configured and can never report NoVault: completion, hook install, skill install, skill list, skill show, and the hidden __complete-paths helper. doctor also runs without a resolved vault, because it resolves the vault itself and reports a failure as a check result rather than as an error.
That last point has a practical consequence. doctor is the natural command to diagnose a missing vault, and it will tell you about one through its vault-detected check with status fail while still exiting 0. Parse its output, do not branch on its exit code.
Retired codes
Codes 7 (Locked) and 10 (EncryptedVaultRequired) existed in earlier versions, when a vault was an encrypted container that had to be unlocked before any command would run. Vaults are now ordinary folders of Markdown files on disk, so there is nothing to unlock and no locked state to report. Both codes are retired, and the numbers will not be reused for anything else. If your scripts still branch on 7 or 10, you can delete those branches.
Exit code 2 has two different meanings
This is the sharpest edge in the whole contract, and it is worth understanding before you write any error handling.
The argument parser the CLI is built on uses exit code 2 for its own usage errors, and that collides with HashConflict. A missing required argument, an unrecognized flag, or an edit invocation that supplies no operation selector all exit 2 with plain human-readable text on standard error, not with the JSON error object.
jotura read
jotura --bogus-flag
jotura edit notes/a.md
The first and the third print the missing-argument form. For jotura read that is:
error: the following required arguments were not provided:
<PATH>
For jotura edit notes/a.md the same form names the whole operation selector group, which is the least obvious of the three:
error: the following required arguments were not provided:
<--replace <OLD>|--replace-line <N>|--replace-lines <N:M>|--insert-before <N>|--insert-after <N>|--delete-lines <N:M>|--append|--prepend|--apply <FILE>>
The second prints a different form:
error: unexpected argument '--bogus-flag' found
The reliable way to tell a usage error from a conflict is to test whether the last line of standard error parses as JSON. A real HashConflict always ends with a valid single-line JSON object whose code field is the string HashConflict. A usage error never produces valid JSON on any line.
jotura edit notes/today.md --replace "old" --with "new" --if-hash "$HASH" 2>/tmp/jotura-err.txt
status=$?
if [ "$status" -eq 2 ]; then
if tail -n 1 /tmp/jotura-err.txt | jq -e 'select(.code == "HashConflict")' >/dev/null 2>&1; then
echo "hash conflict: re-read the file and retry the edit"
else
echo "usage error, not a conflict:"
cat /tmp/jotura-err.txt
fi
fi
Reading only the last line keeps the check correct even when -v has prefixed the stream with its vault: progress line. A usage error means your command was malformed, so retrying it unchanged will fail identically. A hash conflict means the file moved under you, so retrying after a fresh read is exactly the right move. Confusing the two produces an infinite retry loop, which is why the check matters.
The error object
Every error the CLI classifies is written to standard error as one compact line of JSON. This happens whether or not you passed --json, so a script never needs to opt into machine-readable errors. Usage errors from the argument parser are the exception described above, and --verbose can add a plain-text line before the JSON one.
The envelope has two fields that are always present:
| Field | Type | Meaning |
|---|---|---|
code | string | The stable symbolic name from the exit code table above, for example HashConflict or Ambiguous. Generic failures use the string Error. |
message | string | A human-readable description. Useful for logs and for the wide InvalidArgs bucket, where the message is the only thing that distinguishes one cause from another. |
Some codes add extra fields alongside those two. They are flattened into the same object rather than nested under a data key.
code | Extra fields |
|---|---|
HashConflict (single file) | expectedHash, actualHash, expected, actual, path (present when the operation knew the path), currentVsIntended (a unified diff of what is on disk against what you were about to write, present when the operation could compute one) |
HashConflict (from batch) | conflicts, an array of objects each carrying path, expectedHash, actualHash, and an optional currentVsIntended |
TargetMissing | path |
NotFound | needle |
Ambiguous | needle, occurrences, lineHits (one-indexed line numbers of every match) |
OutOfRange | line, lineCount |
MultiEditOpFailed | opIndex (zero-indexed), underlyingCode, underlying (an object with the original error’s code, message, and data) |
ReadOnlyShare | path, shareId, mountRel |
AgentConflict | conflicts, an array of vault-relative path strings |
| every other code | none; the object is just code and message |
Two details are worth calling out. The expected and actual fields on a hash conflict are legacy duplicates of expectedHash and actualHash, kept so older scripts keep working, so prefer the longer names in new code. And the OutOfRange fields are overloaded when the failure came from --nth: in that case line holds the N you passed and lineCount holds the number of occurrences found, not a line count. A client that renders “line X of Y lines” from those two fields will print nonsense for --nth failures.
A full hash conflict looks like this, reformatted here for readability but emitted on one line:
{
"code": "HashConflict",
"message": "hash conflict: expected abc123, actual def456",
"expectedHash": "abc123",
"actualHash": "def456",
"expected": "abc123",
"actual": "def456",
"path": "notes/today.md",
"currentVsIntended": "--- a/notes/today.md\n+++ b/notes/today.md\n..."
}
Recovering from the common failures
Exit 2, hash conflict
Someone else wrote the file between your read and your write. The other writer might be the desktop app, the sync loop pulling a remote change, or a second agent. Re-read, re-derive, retry:
HASH=$(jotura read notes/today.md --json | jq -r .hash)
jotura edit notes/today.md --replace "old" --with "new" --if-hash "$HASH"
Writes that carry a hash precondition are atomic against every other Jotura writer, because the CLI and the desktop app serialize on a shared per-vault lock held across the hash comparison and the write itself. Two writers that validate the same hash can never both succeed. Exactly one wins and the other gets exit 2.
Exit 4, ambiguous replace
The --replace needle appeared more than once. The error object tells you where:
jotura edit notes/a.md --replace "TODO" --with "DONE"
That might report occurrences: 3 and lineHits: [12, 40, 91]. Pick one of three fixes: add --all to change every occurrence, add --nth 2 to change a specific one (the count is one-indexed), or extend the needle until it is unique.
Exit 5, out of range
Either a line number exceeded the file, or --nth exceeded the occurrence count. Re-read the file to get its current lineCount before recalculating. Remember that inside an edit --apply sequence the operations run one after another against the evolving content, so line numbers shift as earlier operations add or remove lines.
Exit 11, one operation in a sequence failed
The error object names the failing operation by index and nests the original error inside it:
{
"code": "MultiEditOpFailed",
"message": "op 2 failed: Ambiguous",
"opIndex": 2,
"underlyingCode": "Ambiguous",
"underlying": { "code": "Ambiguous", "message": "...", "data": {} }
}
For edit --apply, exit 11 means nothing at all was written. The whole sequence is computed in memory and committed as one atomic write at the end, so a failure anywhere leaves the file untouched. For batch, the guarantee is narrower and you should not assume it is the same. See the next section.
Exit 14, the target went missing
An --if-hash write found that the file had been deleted or moved since your read, and the CLI refused to recreate it. Decide deliberately: re-read to confirm the file is gone, then either write without --if-hash or abandon the change.
One asymmetry is worth knowing. write --if-hash against a path that does not exist skips the CLI’s own precondition comparison, because that check only runs when a current file was read. The hash is still passed down to the shared write path, which requires the file to exist under an update precondition, so the call surfaces as exit 14 rather than silently creating the file.
Exit 16, agent config conflict
jotura agents sync completed all its non-conflicted work and then exited 16. This is a report, not an abort. Read the conflicts array, decide which side should win, and re-run:
jotura agents sync --prefer vault
What “atomic” means for batch
The batch command describes itself as applying operations atomically, and that is true of its preconditions but not of its apply phase. The distinction matters when you are writing recovery logic.
batch runs in phases. First it plans: it reads current content for every operation that modifies a body and computes the intended result. Failures inside an operation, such as a needle that does not match or a line range that does not exist, surface here as exit 11 before anything is written. Second it verifies every ifHash precondition in one pass; any mismatch aborts the entire batch with exit 2 and a conflicts array, leaving the disk untouched. Third it checks the share guards, so a viewer-mount target or a mount-structure violation aborts the whole batch before any write.
Then it applies. Each operation is its own atomic file write. If an operation fails during this final phase for an unexpected reason, the operations that already succeeded in the same batch are not rolled back. The three preflight phases make this uncommon, but they do not make it impossible. If a batch fails partway through the apply phase, verify the actual state on disk rather than assuming the vault is unchanged. Full detail on batch input shapes is in Editing notes.
The --json flag does not control every command
There are three distinct behaviors, and knowing which group a command is in saves you from parsing output that was never going to be text. Remember that --json is global, so its position on the command line never matters.
Group one respects --json. These print human-readable text by default and structured JSON when you ask:
read, ls, search, quick-open, search-documents, ls-documents, grep, backlinks, links, frontmatter get, tag list, today (bare, --folder, --date), today --read, conventions, memory recall, memory list, context, trash list, templates list, templates show, doctor, shares list, sync status, sync drop-path, skill list, agents status, agents sync.
Group two always emits JSON, and --json does nothing. Nearly every command that changes something is here:
status, vault info, write, edit, create, rename, delete, frontmatter set, frontmatter delete, tag add, tag remove, today --append, memory log, memory supersede, memory resolve, hook install, sync login, sync logout, import, regenerate-md-store, templates create, templates delete, trash <path>, trash restore, trash empty, trash purge, skill install.
Note that today straddles the two groups. The bare form and its --folder, --date, and --read variants respect --json, while --append emits JSON unconditionally.
Group three ignores --json and uses a fixed format of its own. batch always prints pretty JSON. watch always prints compact newline-delimited JSON, one object per line. skill show, completion, and the hidden __complete-paths helper always print plain text and have no JSON form at all.
Most JSON output is pretty-printed as a single document. Four commands emit newline-delimited JSON instead, one compact object per line, because they report a stream of independent results rather than one answer: watch, agents status, agents sync, and skill install.
JSON shapes by command
Field names are camel case throughout. Optional fields marked with a question mark are omitted entirely when they do not apply, rather than being present and null, unless the description says otherwise.
Reading
| Command | Shape |
|---|---|
read | { path, hash, frontmatter, body, lineCount, share? }. frontmatter is null when the file has none, and always null for files that are not Markdown. lineCount counts lines of the body, not the whole file. share appears only when the path routes into a share mount and carries shareId, name, and role. This is the hash source for the safe edit loop. |
ls | An array of { path, isFolder, size, mtime, shareId?, shareRole? }, sorted by path. Folders appear as entries with size zero. The share fields are populated only in JSON mode and only inside a share mount. |
search (keyword mode) | An array of { path, title, snippet, score }. |
search (semantic or smart mode) | An array of { path, headingPath, snippet, score }. Note the shape differs from keyword mode: title is replaced by headingPath. |
quick-open | An array of { path, score }. |
search-documents | An array of { documentPath, mdStorePath, title, snippet, score }. |
ls-documents | An array of { path, size, hasMdStore, conversionOk }. The text form prints the path and the conversion status separated by a tab. |
grep (default) | An array of { path, line, lineText, matchedText }. With -c it is an array of { path, count }; with -l or -L it is a bare array of path strings. |
backlinks | { target, hits } where each hit is { path, line, snippet, linkKind }. |
links | { source, links } where each link is { line, snippet, target, linkKind, resolved }. resolved is an array of candidate vault paths and is empty for a dead link. |
frontmatter get (whole block) | { path, frontmatter }, with frontmatter null when the file has none. |
frontmatter get (single key) | { path, key, value }, with value null when the key is absent. |
tag list | A bare array of tag strings. |
Full flag detail for these commands is in Reading and searching.
Writing
| Command | Shape |
|---|---|
write | { path, hash, diff? }. |
edit (single operation) | { path, hash, changes, diff? }. |
edit --apply | { path, currentHash, newHash, changes, opsApplied, diff? }. The key names differ from the single-operation form. A client parsing edit output must branch on which form it invoked. |
create | { path, template?, diff? }. The returned path is the final one, including any collision suffix such as (2). |
rename | { from, to, diff? }. |
delete | { path, deleted: true }, or { path, trashed: true } with --trash. |
batch | { opsApplied, results } where each result is { path, kind, currentHash?, newHash?, changes?, diff?, newPath? }. newPath appears for create and rename operations. |
frontmatter set | { path, hash, key, diff? }. |
frontmatter delete | { path, hash, key, deleted, diff? }. Deleting a key that was not there is a success with deleted false and no write. |
tag add / tag remove | { path, hash, tag, added } or { path, hash, tag, removed }. Both are idempotent: adding a tag that exists, or removing one that does not, succeeds with the boolean false and writes nothing. |
import | { path, contentHash, size, converted, mdStorePath?, note? }. mdStorePath appears only when a Markdown mirror was attempted and the file type is eligible. note appears only when conversion was attempted and did not succeed. See Documents from the CLI. |
regenerate-md-store | An array of { path, mdStorePath, converted, skipped?, reason? }. reason appears only with --stale-only and is one of Missing, SourceChanged, ConverterUpgraded, or PreviousFailure. |
The operation surface behind these shapes is documented in Editing notes, Frontmatter and tags, and Creating, moving, and deleting.
Vault, sync, and shares
| Command | Shape |
|---|---|
status | { vault, noteCount, sync: { enabled, keyCached, paused }, shares: { count } }. |
vault info | { vault, noteCount, totalSize, sync: { ... }, shares: { count } }. totalSize is the summed byte size of all synced files. |
sync status | { enabled, keyCached, paused, pendingOps, shares } where each share entry is { shareId, name, role, pendingOps, paused }. This is the richer form; status reports only the three-field sync summary. |
sync login | { status: "keyCached", vault }. |
sync logout | { status: "keyForgotten", vault }. |
sync drop-path | { path, isFolder, enqueued, note }. Without --json the text form is a single line, enqueued sync delete for <path> (no local file touched). |
shares list | An array of { shareId, name, mountRel, kind, role, isOwner, mountExists, syncDbExists, pendingOps, paused, keyCached }. |
Daily notes, templates, and trash
| Command | Shape |
|---|---|
today (bare or with --folder / --date) | { path, hash, created }. The text form prints only the path. |
today --read | { path, hash, body, frontmatter, created }. |
today --append | { path, hash, created, appended: true }. |
templates list | { templates }, an object wrapping an array of names, not a bare array. |
templates show | { name, path, frontmatter, body }. |
templates create | { name, path }. |
templates delete | { name, deleted: true }. |
trash <path> | { path, trashed: true }. |
trash list | An array of { originalPath, isFolder, size, trashedAt }. trashedAt is an integer epoch timestamp, not a string. The text form prints the original path and that timestamp separated by a tab. |
trash restore | { originalPath, restoredAt }. Despite its name, restoredAt holds the destination path, not a timestamp. |
trash empty | { purged }, a count. |
trash purge | { originalPath, purged: true }. |
See Daily notes, tasks, and templates for how these surfaces behave in the desktop app.
Agent memory and configuration
| Command | Shape |
|---|---|
memory log | { path, hash, kind, similar } where each similar entry is { path, snippet }. The similar list is an advisory duplicate warning computed before the write; it never blocks the write. |
memory recall | An array of { path, kind, status, importance, project, date, snippet, score }. |
memory list | An array of { path, kind, status, importance, project, date }. No snippet and no score. |
memory supersede | { superseded, by }. |
memory resolve | { resolved }. |
conventions | { path, hash, body, created }. Note that body has the frontmatter stripped and there is no separate frontmatter field, unlike read. |
context <task> | { conventions, openFollowups, memories, agentConfigDrift }. With a task argument, each memories entry carries the full recall shape: path, kind, status, importance, project, date, snippet, score. |
context (no task) | The same four keys, but each memories entry carries path, kind, project, and date only. A client must branch on which form it called. |
hook install | { agent, path, action } where action is installed or unchanged. |
agents status | Newline-delimited JSON, one line per file: { item, action, path, system, custom }, followed by a { warnings } line when there are any. Always exits 0, including when it reports conflicts. |
agents sync | The same newline-delimited shape, plus "dryRun": true on each line under --dry-run. Exits 16 when any file conflicted. |
skill install | Newline-delimited JSON, one line per file touched: { agent, path, action } where action is installed, updated, or unchanged. |
skill list | An array of per-agent entries. The claude entry is richer than the others, adding notesSkillPath, notesSkillState, agentNotesPath, agentNotesInstalled, and agentNotesUpToDate to the common agent, path, installed, and upToDate fields. |
Two fields in that table need a caveat. In both context forms, conventions is the full text of the conventions note, or an empty string when the note does not exist. And agentConfigDrift is true only when jotura agents sync has run at least once on this machine and the managed files have since drifted, so a value of false also covers “agents sync has never run here”. The agent surface explains that state file.
One behavior outside the JSON contract is worth knowing. skill show prints the canonical SKILL.md and nothing else, not the combined body that the codex, gemini, and copilot managed blocks actually receive. Diffing its output against an installed instruction file will show a mismatch that is not a broken install.
Diagnostics
| Command | Shape |
|---|---|
doctor | An array of { name, status, detail } where status is pass, warn, fail, or info. doctor always exits 0, even when a check has status fail, so a script cannot use its exit code as a health gate. Parse the statuses instead. |
watch | Compact newline-delimited JSON, one object per line: { kind, path, ts }. kind is Created, Modified, or Deleted. ts is an ISO 8601 timestamp in UTC to second precision. |
Both of these are covered in more depth in Watching and diagnostics.
A quick health gate that reads the statuses rather than the exit code:
jotura doctor --json | jq -e 'all(.status != "fail")' >/dev/null && echo "vault healthy"
Dry-run output
Commands that support --dry-run compute the change, write nothing, and emit one of two envelopes. Which one you get depends on whether the operation has textual content to preview.
The content envelope is used by write, edit, frontmatter set, frontmatter delete, tag add, and tag remove:
{
"dryRun": true,
"path": "notes/a.md",
"currentHash": "...",
"newHash": "...",
"changes": 1,
"preview": { "kind": "replace", "before": "...", "after": "...", "lineRange": [5, 8] },
"diff": "..."
}
The preview object varies by operation kind. A replace gets before and after. The line-oriented operations add lineRange. The insert, append, and prepend operations get after only. A line deletion gets before and lineRange. An opsApplied field appears only for edit --apply.
The manifest envelope is used by create, rename, and delete, which have no old body to diff against a new one. All three share dryRun, path, kind, and an optional diff, but the remaining fields differ by command:
| Command | Manifest fields |
|---|---|
create --dry-run | { dryRun, path, kind: "create", parent, name, template?, diff? } |
rename --dry-run | { dryRun, path, kind: "rename", from, to, diff? } |
delete --dry-run | { dryRun, path, kind: "delete", permanent: true, diff? } |
delete --trash --dry-run | { dryRun, path, kind: "trash", trash: true, diff? } |
Manifest diffs are fixed placeholder text rather than real unified diffs. A rename produces --- a/<path>, +++ b/<dest>, (file renamed). A delete produces --- a/<path>, +++ /dev/null, (file deleted). A create produces --- /dev/null, +++ b/<path>, (file created).
Two dry-run caveats are worth knowing before you rely on a preview to predict a real run. A create --dry-run does not model the collision suffix, so when the target already exists the dry run reports the unsuffixed path while the real run would return the suffixed one. And both delete --dry-run and rename --dry-run read their target first, so a path that does not exist exits 3 under a dry run, while a real delete of a missing path returns 0 silently.
Reading input from standard input
Several commands accept content on standard input rather than as a flag value. The conventions differ slightly by command, so the table below is exhaustive.
| Command | How input is read |
|---|---|
write <path> | Reads standard input to end of file when --content is omitted. There is no bare positional alternative: it is either --content or standard input, always. |
edit --insert-before, --insert-after, --append, --prepend | Reads standard input to end of file when --content is omitted. |
edit --apply - | The literal value - means read the operation list JSON from standard input. |
batch - | The literal positional - means read the batch JSON from standard input. |
import - | The literal positional - reads raw bytes from standard input. This form requires --as <vault-path>, because there is no filename to derive a destination from. Omitting it exits 9. |
memory log --body - | The literal value - means read the note body from standard input. Any other value is used verbatim as the body text. |
today --append with no value | A bare --append is the sentinel for “read from standard input”. A consequence is that you cannot append a genuinely empty string this way. |
templates create <name> without --from | Reads standard input to end of file and uses it as the template body. |
sync login --passphrase-stdin | Reads standard input to end of file, then strips a trailing carriage return and newline. Without the flag, the password is prompted for interactively with no echo. |
Worked examples of each form:
echo "# New note" | jotura write notes/new.md
jotura edit notes/a.md --append < footer.md
jotura edit notes/a.md --apply - < ops.json
jotura batch - < operations.json
jotura import - --as attachments/report.pdf < ~/Downloads/report.pdf
jotura memory log --kind decision --title "Chose Tantivy" --body - < rationale.md
date | jotura today --append
jotura templates create meeting < meeting-template.md
One parsing note that saves confusion: the --content, --replace, --with, and today --append flags all accept values beginning with a hyphen without needing a -- separator, so replacing a string like --verbose works directly.
Writing a robust caller
Putting the pieces together, a script or agent that drives the CLI safely does four things.
It always passes --json on read commands, because parsing tab-separated text is fragile and the structured form is stable. It always captures standard error separately from standard output, because the error object lands on stderr while the success payload lands on stdout. It branches on the numeric exit code first and falls back to the code field only for detail. And it treats exit 2 as ambiguous until it has checked whether the last line of standard error parses as JSON.
out=$(jotura read notes/today.md --json 2>/tmp/jotura-err.txt)
status=$?
case "$status" in
0) HASH=$(printf '%s' "$out" | jq -r .hash) ;;
3) echo "note does not exist"; exit 1 ;;
6) echo "no vault; set JOTURA_VAULT or pass --vault"; exit 1 ;;
*) tail -n 1 /tmp/jotura-err.txt | jq -r '.code + ": " + .message'; exit 1 ;;
esac
jotura edit notes/today.md --append --content "- new item" --if-hash "$HASH"
Related pages
- Install and setup for getting the binary on your path and pointing it at a vault.
- Editing notes for the full edit and batch operation surface that produces most of these codes.
- Reading and searching for the search modes behind exit code 13.
- Sync and shares for the sync and share state behind exit codes 12 and 15.
- The agent surface for the agent configuration reconciliation behind exit code 16.
- Troubleshooting for problems that show up in the desktop app rather than at the command line.