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 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, or a bare note name passed to read or edit matched more than one note. For a --replace conflict, add --all, add --nth N, or lengthen the needle until it is unique; the error object lists every matching line number. For a bare-name conflict, pass the full path instead; the error object lists the candidate notes.
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. Two 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 or a usable embedder. The specific cause varies: no vector database, semantic search disabled in that database, a missing local embedding model, no resolvable configuration directory, an unavailable machine learning runtime, or an index built with OpenAI embeddings and no API key (semantic index uses OpenAI embeddings; set OPENAI_API_KEY or add the key in the desktop app (Settings -> Semantic search)) or a rejected one (OpenAI rejected the API key; update it in the desktop app or OPENAI_API_KEY). The message names the exact missing prerequisite. Enable semantic search in the desktop app, where the provider is chosen. For an OpenAI index, set OPENAI_API_KEY or save the key in the 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.

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 covers many causes. Four of them 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, write with neither --content nor anything on standard input, 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, a wrong sync password, and an invalid recovery phrase.

Commands that never exit 6

Two commands are handled before vault resolution runs, so they work with no vault configured and can never report NoVault: completion 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.

doctor is the natural command for diagnosing a missing vault, and it reports one through its vault-detected check with status fail while still exiting 0, so a script should parse its output and ignore 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.

Code 16 (AgentConflict) belonged to jotura agents sync, which copied agent config files between the vault and each tool’s home folder. Jotura no longer manages agent config, so that command and its exit code are gone, and 16 will not be reused either. If your scripts still branch on 7, 10 or 16, you can delete those branches.

Exit code 2 has two different meanings

This is the sharpest edge in the contract, so settle it 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. Retrying a usage error unchanged fails the same way every time, because the command itself is malformed, while a hash conflict means the file changed under you and a retry after a fresh read is the correct response. A script that treats them alike will retry a malformed command forever.

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 (--replace conflict) needle, occurrences, lineHits (one-indexed line numbers of every match)
Ambiguous (bare-name conflict) needle (the name you passed), occurrences, candidates (an array of vault-relative paths); lineHits is present but always empty in this case
OutOfRange line, lineCount
MultiEditOpFailed opIndex (zero-indexed), underlyingCode, underlying (an object with the original error’s code, message, and data)
ReadOnlyShare path, shareId, mountRel
every other code none; the object is just code and message

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. The OutOfRange fields are also 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. Read the file again, rebuild your change against the new hash, and 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. When two writers validate the same hash, 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. batch gives a narrower guarantee, described in 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.

write --if-hash behaves asymmetrically against a path that does not exist. It 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.

What “atomic” means for batch

The batch command describes itself as applying operations atomically, and that claim holds for its preconditions while the apply phase makes a weaker promise, which matters when you write 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 without ruling it out. 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, tags, tasks, clip, today (bare, --folder, --date), today --read, conventions, memory recall, memory list, memory doctor, context, trash list, templates list, templates show, doctor, shares list, sync status, sync drop-path.

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, sync login, sync logout, import, regenerate-md-store, templates create, templates delete, trash <path>, trash restore, trash empty, trash purge.

The exception inside this group is --dry-run: a dry run of write, edit, create, rename, delete, frontmatter set, frontmatter delete, tag add or tag remove prints a plain-text summary unless you pass --json.

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. 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. watch emits newline-delimited JSON instead, one compact object per line, because it reports a stream of independent events.

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, resolvedFrom?, 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. resolvedFrom appears when you passed a bare note name, and carries that name. 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. A binary file such as an image returns { path, absolutePath, kind, mime, bytes } instead, where kind is image or binary.
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, alias }. alias is null unless the hit was matched through a frontmatter alias, in which case it carries the alias that matched.
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. A resolved link also carries absolutePath, the absolute path of its first candidate.
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, resolvedFrom?, diff? }.
edit --apply { path, currentHash, newHash, changes, opsApplied, resolvedFrom?, 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, linksUpdated, linksSkipped, linkErrors, diff? }. linkErrors is an array of { path, error } for links that could not be rewritten; the rename itself has already happened.
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

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, occurred, snippet, score }.
memory list An array of { path, kind, status, importance, project, date, occurred }. 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 }. With a task argument, each memories entry carries the full recall shape: path, kind, status, importance, project, date, occurred, snippet, score.
context (no task) The same three keys, but each memories entry carries path, kind, project, and date only. A client must branch on which form it called.

In both context forms, conventions is the full text of the conventions note, or an empty string when the note does not exist.

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 caveats apply before you rely on a dry run to predict a real one. 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

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

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"