The ledge CLI
The ledge command lists, reads, searches, creates, and appends to notes from any terminal, and runs their code blocks. The running app follows along live, because a CLI write is an ordinary file change.
Install
Run "Install Shell Command (ledge)" from the command palette.
It writes one small launcher, ledge, into ~/.ledge/.server/bin, pointing at this copy of Ledge, and adds that folder to your PATH in your shell's startup file if it is not there yet. Open a new terminal afterwards. If you move the app, run it again.
On Windows there is nothing to install. The CLI runs in WSL, where the app installs its server, and a new WSL terminal already finds ledge. ledge <title> there opens the Windows app, once it has started at least once (Ledge on Windows).
The same launcher is what lets your phone reach this computer's notes (Ledge on Your Phone): ledge serve over ssh is how a Ledge app reaches any machine, and ledge reads the notes on the machine it runs on. That is this computer's from a terminal here, and a server's from a terminal there, where the server install already put ledge on the PATH (Keep Notes on a Remote Server). ledge help lists the server verbs beside the notes verbs.
The verbs
ledge help prints the full usage.
| Verb | What it does |
|---|---|
ledge ls | Lists notes. |
ledge search <query> | Prints path:line: match rows like grep, and exits nonzero on no hits. |
ledge cat <title> | Prints a note's Markdown. |
ledge tags | Lists the workspace's tags with counts. ledge tags <name> lists the notes bearing one. |
ledge workspaces | Lists the workspace roots. |
ledge new <title> | Creates a note, with the body piped on stdin or stamped from --template, in the folder you are standing in or the one -f names. |
ledge append <title> | Appends to a note, or to one heading's section with --heading. |
ledge run <title> | Runs a note's code blocks in this terminal. |
ledge today | Opens today's daily note in the app. |
ledge <title> | Opens the app at that note. ledge alone just opens the app. |
ledge version | Prints the version of this copy of Ledge, as a bug report wants it. ledge --version is the same. |
ledge backup | Backs this machine up to a bucket: setup, now, status, snapshots, restore, paths, restic (Keep Notes on a Remote Server). |
In a terminal, once the shim is on your PATH:
ledge ls
ledge search "spawn params"
ledge cat "Shipping Notes"Notes are addressed by title. An argument ending in .md is treated as a path instead.
ledge new "Standup" --template "Meeting"
git log --oneline -5 | ledge append "Release Notes" --heading "Shipped"--template stamps the usual {{tokens}} (see Daily Notes and Templates). Titles never clobber: a duplicate gets a numbered file, the same as in the app.
Scope: workspace, folder, and note
Run ledge from inside a workspace folder and it scopes itself there. ls and search cover that workspace, and new creates in it.
Stand in a folder inside the workspace and it narrows one more step, the way any other shell command works on the directory you are in:
cd ~/Notes/projects
ledge ls # only the notes in projects, and below it
ledge search "rate limit" # only that folder
ledge new "API Rollout" # creates ~/Notes/projects/api-rollout.md-f <folder> says it outright, from anywhere. ledge ls -f admin lists that folder, and ledge new "Expenses" -f admin/2026 creates the folder if it is new. See Notes and Workspaces for folders in the app.
-f is also how you say which of two notes sharing a title you mean: ledge cat "Plan" -f projects. The folder you are standing in never does that, only -f, so a title always reaches the whole workspace no matter where you run it from.
ledge today is the one exception. It takes -f, which overrides the daily.folder setting for that call, but it ignores the folder you are standing in: today's note is found by its date, and where it lives should not depend on where you happened to be when you first ran it (Daily Notes and Templates).
Inside a note's terminal drawer it also knows the note, so a bare ledge append -m "TODO: check the logs" appends to the note the terminal belongs to.
Three flags override the scope: -w <workspace> targets a specific workspace, -f <folder> targets a folder, and --all widens ls and search to every workspace.
Run a note's blocks
ledge run runs a note's code blocks in the terminal you type it in. Their output goes to that terminal, and ledge run exits with the status of the last block it ran, so it works in scripts and CI.
| Command | What runs |
|---|---|
ledge run Deploy | Nothing. It lists the note's runnable blocks, numbered. In a terminal it then asks which one to run. |
ledge run Deploy -b 2 | Block 2 of that list. |
ledge run Deploy -b migrate | The block whose fence says name=migrate. |
ledge run "Deploy#Rollback" | Every block in the Rollback section, in order. |
ledge run Deploy --all | Every runnable block in the note, top to bottom. |
A block gets a name on its fence, after the language:
```sh name=migrate
./migrate.sh
```Several blocks run in one shell, so a cd or an export in one block reaches the next, as it does in the app. The run stops at the first block that exits nonzero, names that block on stderr, and exits with its status. Blocks marked norun, and blocks with no closing fence, never run (Keep a block from running).
The blocks run in the note's cwd:, or in the directory you ran ledge run from when the note has none. The environment is your terminal's, with the note's env:, envFile: and profile on top (Frontmatter and Environments). Your shell's startup files are not read, so aliases and functions defined there are not available.
A block marked confirm shows its code and asks before anything runs. With no terminal to ask in, as in CI, ledge run refuses such a block unless you pass --yes.
A note whose host: line names another machine is refused for now (Run Code on Remote Hosts), and so is a note whose sudo: line names a user (Frontmatter and Environments). Run it from the app.
Inside a note's terminal drawer, ledge run with no title means that note.
Piping and JSON output
Results go to stdout and everything conversational to stderr, so pipes stay clean. --json switches any verb to machine-readable output.
The CLI dispatches through the same handlers as the MCP tools (Agents and Ledge), so it follows the same rules: titles resolve the same way, locked notes refuse their bodies, and there is no delete verb.
That makes it an agent surface in its own right. An agent that can run shell commands can work your notes with ledge alone, with no MCP setup.