omajot

Command line

Your notes in the terminal

omajot is one binary. Its commands read and change your notes from a terminal or a script, and it runs the hub and the desktop daemon.

Note commands

These commands work on the notes of this computer. They use the omajot daemon: the one that the plugin started, or, when none runs, one that the command starts in the background (it stops after 10 minutes without use). Changes sync to your other devices like changes that you type in the plugin.

omajot <command> [arguments] [--json] [--data <dir>] [--socket <path>] [--no-start]

Address a note as Folder/Subfolder/Title (the title is the first line of the note, without # ), as only Title, or by its id (n-…, from ls -l or --json). Case does not matter when nothing matches exactly. When a name matches two notes, the command stops with exit code 2 and lists them with their ids.

Changes merge. write, edit, append, replace and restore apply only the parts of the text that change. When your phone changes another part of the same note at the same time, both changes stay.

CommandWhat it does
lsLists the folders and notes in a folder.
catWrites the Markdown text of a note to stdout, exactly as stored.
searchFinds the notes whose title or text contains the text.
newMakes a note.
writeMakes stdin the full text of a note.
editOpens the note in your text editor.
appendAdds the text to the end of the note, on a new line.
replaceReplaces the text <old> with <new>.
mvMoves the note to the folder.
rmMoves the note to the Trash.
mkdirMakes the folder and its missing parent folders.
rmdirDeletes an empty folder.
tagsLists the tags (#word in the text) of the notes that are not in the Trash, with the number of notes, the most used first.
historyLists the versions of a note, the oldest first.
restoreMakes the text of an earlier version the current text of the note.
exportWrites every note as a Markdown file: <dir>/Folder/Subfolder/Title.md.
statusShows the daemon (version, how it started), the data directory, the socket, the hub, and the sync state: online, connecting or offline, and the number of changes that the hub did not get yet.

omajot ls

Lists the folders and notes in a folder. Without a folder: the top level. Pinned notes come first, then the notes that changed last.

omajot ls [folder] [-r] [-l] [--tag <tag>] [--trash]
OptionMeaning
-r, --recursiveAlso list the notes in all subfolders, as paths.
-l, --longAlso show the time of the last change and the id.
--tag <tag>Only notes with this tag (with or without #). Includes subfolders.
--trashOnly notes in the Trash. Includes subfolders.
$ omajot ls
$ omajot ls Work -r
$ omajot ls --tag todo -l
$ omajot ls --json | jq -r '.notes[].path'

omajot cat

Writes the Markdown text of a note to stdout, exactly as stored.

omajot cat <note> [--at <time>]
OptionMeaning
--at <time>The text as it was at this time: 2026-09-26 14:03 (local time), 2026-09-26T12:03Z (UTC), 2026-09-26, or a time ago: 90s, 15m, 2h, 3d, 1w.
$ omajot cat "Work/Plans/Q3"
$ omajot cat n-3f9c2a1b7d4e5f60-42
$ omajot cat Groceries --at 2d

omajot new

Makes a note. Its text is # <title>, an empty line, and the text. - reads the text from stdin. Writes the path of the new note.

omajot new <title> [text|-] [--folder <folder>]
OptionMeaning
--folder <folder>Put the note in this folder. omajot makes the folder and its parent folders if necessary.
$ omajot new "Groceries" "- milk"
$ omajot new "Meeting 2026-09-26" --folder Work/Meetings
$ git log -5 --oneline | omajot new "Release notes" - --folder Work

omajot write

Makes stdin the full text of a note. omajot applies only the parts that are different, so changes from other devices in other parts of the note stay. When the note does not exist, omajot makes it: in --folder, or in the folder of the path (Work/Todo goes into Work). When the first line of the text is not the title, omajot adds # <title> at the top, so that the name finds the note again.

omajot write <note> [--folder <folder>] < text
$ omajot cat Todo > todo.md; $EDITOR todo.md; omajot write Todo < todo.md
$ printf '# Status\n\nAll green.\n' | omajot write Work/Status

omajot edit

Opens the note in your text editor. Each time you save, omajot applies your changes to the note, also while other devices change it. omajot applies the last save again when the editor closes. The file is in a private temporary directory, which omajot deletes after the editor closes.

omajot edit <note>

The editor: $VISUAL, else $EDITOR, else vi. The value can have arguments, for example code --wait or nvim -u NONE: omajot runs it as sh -c '<editor> "$1"' omajot <file>. A graphical editor must wait until you close the file (code --wait, subl -w, gedit -s). When no editor is set and vi is not installed, the command fails: set $EDITOR.

$ omajot edit Todo
$ EDITOR="code --wait" omajot edit "Work/Plans/Q3"

omajot append

Adds the text to the end of the note, on a new line. - reads the text from stdin. omajot adds a line break at the end when the text has none.

omajot append <note> <text|->
$ omajot append Groceries "- eggs"
$ date | omajot append Log -

omajot replace

Replaces the text <old> with <new>. <old> must occur exactly one time: when the note does not contain it, exit code 1; when it occurs more than one time, exit code 2 and no change. Add more of the text around <old> to make it unique, or use --all. Case matters. Changes made on other devices at the same time stay.

omajot replace <note> <old> <new> [--all]
OptionMeaning
--allReplace every occurrence.
$ omajot replace Todo "- [ ] call Anna" "- [x] call Anna"
$ omajot replace Notes "2025" "2026" --all

omajot mv

Moves the note to the folder. / is the top level. The folder must exist (see omajot mkdir).

omajot mv <note> <folder>
$ omajot mv Todo Work
$ omajot mv "Work/Old plan" /

omajot rm

Moves the note to the Trash. omajot does not delete notes: you can get them back from the Trash.

omajot rm <note> [--restore]
OptionMeaning
--restoreMove the note out of the Trash.
$ omajot rm "Old idea"
$ omajot rm "Old idea" --restore

omajot mkdir

Makes the folder and its missing parent folders. A folder that exists is not an error.

omajot mkdir <path>
$ omajot mkdir Work/Meetings/2026

omajot rmdir

Deletes an empty folder. When the folder has notes or folders, the command stops with exit code 3.

omajot rmdir <folder> [--force]
OptionMeaning
--forceDelete the folder all the same. Its notes move to the top level and its folders to the parent folder. No note is deleted.
$ omajot rmdir Work/Old

omajot tags

Lists the tags (#word in the text) of the notes that are not in the Trash, with the number of notes, the most used first.

omajot tags
$ omajot tags
$ omajot ls --tag todo

omajot history

Lists the versions of a note, the oldest first. A version is a group of changes from one device with less than one minute between them. For each version: the number, the time of its last change, the device, and the number of characters added and removed. Show an old text with omajot cat --at, and make it the current text with omajot restore.

omajot history <note>
$ omajot history Todo
$ omajot restore Todo 3

omajot restore

Makes the text of an earlier version the current text of the note. omajot applies it as a normal change: it syncs to your other devices, and the history keeps every version, so you can undo the restore. <version> is a number from omajot history; <time> is written as for cat --at.

omajot restore <note> <version|time>
$ omajot restore Todo 3
$ omajot restore Todo 2h

omajot export

Writes every note as a Markdown file: <dir>/Folder/Subfolder/Title.md. Attachments go to <dir>/attachments/, and the links in the notes still work. <dir>/README.md tells what the files are. The export is a copy: changes to the files do not go back into omajot. omajot changes characters that file names cannot have to -. Notes with the same title get (2), (3), … at the end of the name.

omajot export <dir> [--trash] [--force]
OptionMeaning
--trashAlso write the notes in the Trash, to <dir>/Trash/.
--forceWrite into a directory that is not empty.
$ omajot export ~/notes-backup

omajot status

Shows the daemon (version, how it started), the data directory, the socket, the hub, and the sync state: online, connecting or offline, and the number of changes that the hub did not get yet.

omajot status
$ omajot status
$ omajot status --no-start

omajot tui

A terminal UI for your notes, like the plugin's main window: the folders, the tags and the Trash on the left, the notes in the middle, the note as formatted Markdown on the right. It uses the daemon like the other commands and starts one when none runs. When a note changes on another device, the TUI shows the change at once. The status bar shows the sync state: synced, syncing, connecting, offline, or local only.

omajot tui in Ghostty: folders and tags, the note list, and a note with a picture, a table and a checklist
omajot tui in Ghostty. The picture uses the Kitty graphics protocol; the colours come from the Omarchy theme.
omajot tui [--data <dir>] [--socket <path>] [--no-start] [--hub <url> | --no-hub]
KeyWhat it does
j k, ↓ ↑Moves down and up in the column.
h l, ← →, TabGoes to the column on the left or right.
g GGoes to the first or the last item.
d uScrolls the note down or up.
/Searches the title and text of all notes. Enter keeps the result, Esc clears it.
e, EnterOpens the note in your text editor, as omajot edit does.
nMakes a note: type the title, then the editor opens. The note goes into the folder that you are in.
pPins or unpins the note. Pinned notes come first.
xMoves the note to the Trash. In the Trash, x restores it.
mMoves the note to a folder that you select.
NMakes a folder, inside the selected folder.
rRenames the selected folder.
?Shows all keys.
qQuits.

Editing. The TUI gives the terminal to your editor ($VISUAL, else $EDITOR, else vi) and comes back when the editor closes. Each save goes into the note at once. When the note also changes on another device while you edit, omajot keeps both changes and the status bar says Saved and merged. The Omarchy plugin and the web app can have the same note open at the same time.

Pictures show in terminals with the Kitty graphics protocol (Kitty, Ghostty, WezTerm). Other terminals show a line with the name of the picture. Colours come from the current Omarchy theme, else from a built-in dark theme. Set NO_COLOR to use the colours of your terminal. The icons need a Nerd Font, as in Omarchy.

The TUI needs at least 20 × 3 cells. From 110 columns it shows three columns, from 70 columns two, below that one. It gives the terminal back in all cases: when you quit, after an error, and on SIGTERM.

JSON output and exit codes

With --json, every command writes one JSON object to stdout. Scripts and AI agents use it. SKILL.md shows every shape; the most used:

# omajot ls -r --json
{"ok":true,"folder":null,"folders":[],"notes":[{"id":"n-…","title":"Todo","path":"Work/Todo",
  "folder":"f-…","tags":["work"],"pinned":false,"trashed":false,"created":1790000000000,
  "updated":1790000100000,"snippet":"- call Anna"}]}
# omajot cat Todo --json
{"ok":true,"id":"n-…","path":"Work/Todo","text":"# Todo\n\n- call Anna\n","at":null}
# omajot write / append / replace / new / mv / rm --json
{"ok":true,"id":"n-…","path":"Work/Todo","changed":true,"action":"updated"}
# a failure, here an ambiguous name (exit code 2)
{"ok":false,"exit":2,"error":"ambiguous","candidates":[{"id":"n-…","path":"Work/Todo","trashed":false},…]}
Exit codeMeaning
0Done.
1Not found: the note, the folder, the text for replace, or no match for search.
2Ambiguous: the name matches more than one note or folder, or replace found the text more than one time.
3Conflict: rmdir of a folder that is not empty, export into a directory that is not empty, or a note that another program has open for editing.
64Usage: a missing or wrong argument.
69No daemon: none runs and none could start (or --no-start).
70Another error.

omajot hub

Runs the hub: it stores all changes, sends them to your devices, stores attachments and serves the web app.

omajot hub [--port 8787] [--data <dir>] [--login <tailscale login> | --no-auth]
           [--web <dir>] [--bind 127.0.0.1] [--url <public url>] [--timeout-ms N]
FlagDefaultMeaning
--porthub_port in the config file, else 8787The port on the bind address.
--datahub_data in the config file, else ~/omajot-dataThe folder for batches.jsonl (all changes) and blobs/ (attachments).
--loginhub_login in the config fileOnly requests with this Tailscale-User-Login header may use /api/*. tailscale serve sets the header.
--no-authoffNo login check, for local tests. The hub refuses to start with it on an address other than loopback.
--webthe web app in the binaryServe the web app from this folder instead, for example web/dist while you work on it. The hub reads the folder when it starts.
--bind127.0.0.1The IPv4 address to listen on. Keep loopback when you use tailscale serve.
--urlhub in the config file if it is an https URL, else from tailscale serve statusThe URL for phones. The hub prints it with a QR code when it starts.
--timeout-ms30 000The deadline for normal requests, in milliseconds. The event stream has its own deadline of 600 s and ends cleanly 10 s before it.

The hub needs a login (--login or hub_login) or --no-auth. --no-auth ignores hub_login. With the config file, a plain omajot hub is enough, also for a service. Stop the hub with Ctrl+C: it finishes the open requests first.

omajot daemon

The plugin runs the daemon for you, and the note commands start one when none runs. The daemon keeps the local copy of your notes, syncs it with the hub, and handles paste. It speaks JSON lines on stdin and stdout (for the plugin) and on a unix socket (for the note commands), so you can also use it from scripts.

omajot daemon [--hub <url> | --no-hub] [--data <dir>] [--socket <path>]
              [--background [--idle-exit <seconds>]]
FlagDefaultMeaning
--hubhub in the config fileThe hub URL. Without a hub, the daemon keeps your notes on this computer only.
--no-huboffDo not sync, also when the config file names a hub.
--datadata in the config file, else ~/.local/share/omajotThe local copy: the log of all changes, the queue of unsent changes, and attachments/.
--socketsocket in the config file, else $XDG_RUNTIME_DIR/omajot.sockThe socket for the note commands, readable only by you. Other data folders get omajot-<hash>.sock; without $XDG_RUNTIME_DIR (macOS) the socket is <data>/daemon.sock. --socket "" turns it off.
--backgroundoffDo not read stdin. The note commands start the daemon like this. A plugin daemon that starts later takes over the data folder.
--idle-exit600With --background: stop after this many seconds without note commands (once the hub has all changes). 0: never.

An example session:

$ omajot daemon --no-hub --data /tmp/notes
{"ev":"sync","state":"offline","pending":0,"head":0}
{"id":1,"cmd":"create","folder":null,"text":"# Hello\nfrom a script #demo"}
{"re":1,"ok":true,"note":"n-c48c3ae4cbe96ad4-1"}
{"ev":"notes","upsert":[{"id":"n-c48c3ae4cbe96ad4-1","title":"Hello","snippet":"from a script #demo","tags":["demo"],…}]}
{"id":2,"cmd":"search","q":"script"}
{"re":2,"ok":true,"ids":["n-c48c3ae4cbe96ad4-1"]}

Each request gets one reply ("re"). Events ("ev") can come at any time.

docs/PROTOCOL.md lists all commands and events. The plugin and the web app use the same commands.

Only one daemon can use a data folder: it locks <data>/daemon.lock. A second daemon on the same folder stops with exit code 3. Use the socket to reach the daemon that runs.

omajot qr

Prints a URL and its QR code in the terminal. Without a URL, it uses hub from the config file. Without a configured hub, it shows how to set one up.

omajot qr [url]
$ omajot qr https://your-mac.your-tailnet.ts.net:8443
Open omajot on your phone:
  https://your-mac.your-tailnet.ts.net:8443
                                     
                                     
    █▀▀▀▀▀█  ▀█▀ █▀▄   ▀█ █▀▀▀▀▀█    
    █ ███ █ ██▄▀▀▄ ██▄▀▄▀ █ ███ █    
    █ ▀▀▀ █ █▀▄▀ ▀▀▄█ █▀  █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ █ █ █ ▀▄█ ▀▄▀ ▀▀▀▀▀▀▀    
    █▄██▀▀▀ ▄█▄█ ▀▄▀▄█▄▄█▄█▀█▀▀ ▄    
     █▄▀▄█▀▄▄▀▀█  ▀▀▄ ▀ █ ▀ ▄▄ ▄     
    █ ██▄█▀ ▀▀▀▄▄▀▄ ▄█▄▄█▄▄█▄▀▀ ▄    
      ▀█ ▀▀▀▄ ███ ▀▀▄ ▄▄█ ▀██▀▀▄     
    █▄ ▄▄█▀▄█ ▄█▀▀▄  █▄█▄▄▄█▄▀█ ▄    
    █  █▄▄▀▀▀▄██▄▀▄▀▀▀▄ ▀▀ █  ▀▄     
    ▀ ▀   ▀ ▄  ██▄ ▀█▀▄██▀▀▀█▄███    
    █▀▀▀▀▀█ ▄█▄ ██▀█▄  ██ ▀ █▀▀      
    █ ███ █ ██▀  ▄▀▀█▀  █▀▀▀▀▄███    
    █ ▀▀▀ █ ▀█     ▀▄ ▄▀██▀███▀█     
    ▀▀▀▀▀▀▀ ▀▀ ▀▀▀▀  ▀▀   ▀  ▀▀      
                                     
                                     
Your phone needs Tailscale. It is free for personal use:
  https://tailscale.com/download

The QR code is black on white, also in a dark terminal. Phones scan it best at a normal font size.

The config file

~/.config/omajot/config.json (or $XDG_CONFIG_HOME/omajot/config.json). All fields are optional.

{
  "hub": "https://your-mac.your-tailnet.ts.net:8443",
  "data": "~/.local/share/omajot",
  "socket": "/run/user/1000/omajot.sock",
  "hub_login": "you@example.com",
  "hub_port": 8787,
  "hub_data": "~/omajot-data"
}

hub, data and socket are for the daemon, the plugin and the note commands. hub_login, hub_port and hub_data are for omajot hub on the hub computer; its flags win over them. On the hub computer, omajot hub also uses hub as its phone URL, so it does not need to ask Tailscale.

omajot uses the first value it finds:

  1. the command-line flag (--hub, --data, --socket),
  2. the plugin setting (hubUrl, dataDir), when it is not empty,
  3. the config file,
  4. the default: no hub, and ~/.local/share/omajot for the data.

Tools

tools/import_joplin.py

Imports a Joplin desktop profile. See Import your Joplin notes.

FlagDefaultMeaning
--profile~/.config/joplin-desktopThe Joplin profile to read.
--data~/.local/share/omajotThe omajot copy to import into. Use a separate folder while the plugin runs.
--hub / --no-hubthe config fileWhere the imported notes go.
--dry-runoffShow what the import would do. Change nothing.

tools/seed_sample.py

Puts the sample notes from examples/sample-notes/ into a new copy: 26 notes in 8 folders, with tags, checklists, tables and images. The screenshots on this site use them.

$ tools/seed_sample.py --data /tmp/omajot-demo-replica --hub http://127.0.0.1:8787