# Getting Started
Source: https://ledge.sh/docs
> Ledge is the notebook for developers and DevOps.
Ledge is the notebook for developers and DevOps. It runs code and commands straight from your Markdown.
This manual also ships inside the app, where it is read-only and its code blocks do not run. The note called Welcome to Ledge, which opens on a first launch and on a server that has no notes yet, is where the same examples do run. On the web, the runs are shown.
## Your first note
A note with a shell block in it. ⌘↩ inside the block, or the Run button on it (a tap, on a phone), runs it:
```sh
curl -s https://api.github.com/zen
```
Output:
```txt
Keep it logically awesome.
```
One line of output streams into a panel beneath the block.
That is a real shell, and it is the whole idea: any fenced block whose language is runnable gets a Run button. `sh`, `python`, `node`, and others are runnable out of the box, and the list is a setting. ⇧⌘↩ sends a block to the note's terminal drawer instead. See [Running Code](https://ledge.sh/docs/running-code).
## The shell persists between blocks
Each note keeps one shell for inline runs, so state carries from block to block. A note with these two blocks, run in order:
```sh
cd /tmp
export FLAVOR=nautical
```
```sh
pwd
echo "this shell is feeling $FLAVOR"
```
Output of the second block:
```txt
/tmp
this shell is feeling nautical
```
The second one prints `/tmp` and `this shell is feeling nautical`. The `cd` and the `export` happened in the same shell the second block ran in.
Every note also has a full terminal: ⌃\` opens the drawer. It is a separate shell from the inline one, and it belongs to that note alone.
## Point a note at a project
A note can declare where its shells start. Press ⌥⌘, to add frontmatter:
```
---
cwd: ~/Projects/my-app
env:
NODE_ENV: development
---
```
Every shell the note spawns now starts in that directory with that environment. The blocks run where the code is. Two more lines are worth knowing:
* `profile: name` layers in secrets kept outside the notes folder ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)).
* `host: staging` runs the note's blocks over ssh on that machine, while the note stays here ([Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts)). Keeping the note itself on another machine is a different feature, a server connection ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)).
[Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments) covers the block in full.
To skip frontmatter entirely, attach a project folder as a workspace: run "Attach Folder as Workspace…" from the command palette (⇧⌘P). Its `.md` files become the workspace's notes, and their shells start in the project folder automatically.
## Write, link, and find
A note's first line names it: type `# Shipping Notes` and the file becomes `shipping-notes.md`. Link between notes with `[[Title]]` (typing `[[` opens a picker), and tag them with inline `#hashtags` or a frontmatter `tags:` line.
* ⌘P opens a note by title.
* ⌥⌘P searches full text across the workspace.
* ⌥⌘L shows backlinks, ⌥⌘O the outline, ⌥⌘T the tags.
Notes are ordinary `.md` files in ordinary folders, so git, agents, and shell tools work on them directly. Ledge follows outside changes even mid-edit. See [Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces) for where the files live, and [Finding Things](https://ledge.sh/docs/finding-things) for search, links, and tags.
## What else Ledge does
* **Panes.** ⌘D splits the view right and ⇧⌘D splits it down, so several notes, each with its own shell, sit on screen at once. See [Panes and Tabs](https://ledge.sh/docs/panes-and-tabs).
* **Daily notes and templates.** ⌘J opens today's note. Mark any note `template: true` in its frontmatter and ⌥⌘N stamps new notes from it. See [Daily Notes and Templates](https://ledge.sh/docs/daily-notes-and-templates).
* **Agents.** A CLI launched inside a note's terminal can read and write your notes over MCP, and a `prompt` code fence pipes its text to `claude -p` with ⌘↩. See [Agents and Ledge](https://ledge.sh/docs/agents-and-ledge).
* **Locking.** "Lock This Note…" encrypts a note's body on disk. Sync services, search, and agents see nothing until you unlock. See [Note Locking](https://ledge.sh/docs/note-locking).
* **The CLI.** "Install Shell Command (ledge)" puts `ledge` on your PATH, so `ledge
` opens a note from any terminal and `ledge today` lands in the daily note. See [The ledge CLI](https://ledge.sh/docs/the-ledge-cli).
* **Remote hosts.** A `host:` line sends a note's blocks over ssh to another machine while the note stays here. See [Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts).
* **Remote servers.** Keep your notes on a server and use the app as the window onto it: the server holds the notes and runs the shells, over ssh. See [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server).
* **Your phone.** The same app on an iPhone or iPad, reading and running the notes on that server. See [Ledge on Your Phone](https://ledge.sh/docs/ledge-on-your-phone).
* **Appearance.** Ledge follows your Mac's light or dark setting. To pin one instead, set `appearance.theme` to `"light"` or `"dark"` under This app in Settings (⌘,) and relaunch.
* **Fonts.** `editor.fontSize` sizes note text and `terminal.fontSize` sizes the terminal, both under This app in Settings (⌘,). Relaunch to apply.
## When something goes wrong
Ledge writes a log of each session, and Help > Reveal Log in Finder opens the folder it is in.
Two files sit there.
`ledge.log` is the session running now.
`ledge.previous.log` is the one before it, which is the file you want after a crash: relaunching Ledge starts a new log, and this is where the old one went.
Both are plain text. Attach them to a bug report.
Seven tutorials combine these into working routines: [Tutorial: Run a Project from a Note](https://ledge.sh/docs/tutorial-run-a-project), [Tutorial: A Daily Workflow](https://ledge.sh/docs/tutorial-a-daily-workflow), [Tutorial: Pair with an Agent](https://ledge.sh/docs/tutorial-pair-with-an-agent), [Tutorial: Keep Notes Synced](https://ledge.sh/docs/tutorial-keep-notes-synced), [Tutorial: Set Up a Ledge Server](https://ledge.sh/docs/tutorial-set-up-a-ledge-server), [Tutorial: Back Up Your Notes to S3](https://ledge.sh/docs/tutorial-back-up-your-notes-to-s3), and [Tutorial: Share Notes with a Git Clone](https://ledge.sh/docs/tutorial-share-notes-with-a-git-clone).
---
# Running Code
Source: https://ledge.sh/docs/running-code
> Any fenced code block whose language is runnable gets a Run button, and the shell behind it belongs to the note it sits in.
Any fenced code block whose language is runnable gets a Run button, and the shell behind it belongs to the note it sits in. This page covers what runs where, and how to change both.
## Run a block inline or in the terminal
⌘↩ runs the block under the caret inline. Output streams into the lower half of the block's own card, below a divider, and stays until you dismiss it. Hovering a block shows Run and Copy, and the output half offers Copy Output and Dismiss. A running block's Run button is gray until its run ends, since one block runs one thing at a time. Dismiss is also how you stop a run that is still going: the panel is the only thing that can show it or stop it, so putting the panel away ends the command behind it.
On a touch device the buttons do not wait to be hovered. Every block wears Run and Copy in its top right corner, sized for a finger, and one tap on Run starts it. The card leaves them a lane of their own, so they never sit over the code they run.
⇧⌘↩ sends the block to the note's terminal drawer instead (⌃\` toggles the drawer). There you can keep typing after the command finishes. An inline run ends when its command does.
A block offers to run only once its fence is closed. While the closing \`\`\` is still missing there is no Run button, because what the block contains is not settled: the next closing fence you type anywhere below it becomes this block's end. The third backtick writes the closing line for you, so a block is closed before you have typed a word of it, and a block written above another one never swallows it. Pressing Enter at the end of a fence opener does the same for an opener that arrived some other way, such as a paste.
A block in a note kept on a remote server also needs that server reachable. Once the connection bar reads "disconnected" the Run and terminal buttons are gray and say which machine cannot be reached, and a run that was already going says "Disconnected" until Ledge can ask about it again. A block run while the bar still reads "reconnecting…" goes as soon as the connection is back. See [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server).
On a phone the backtick is not on the letter keyboard, so the block is a verb instead: the code button on the bar above the keyboard, or "Code Block" in the command palette. It writes both fences and the language `sh`, and leaves the caret in the body, so the next thing you type is the command. Type over the `sh` to run something else.
The same verb wraps a selection in a block, and selects the language for you, since the code is already written and `sh` is a guess about it.
## Answer a prompt from a running block
An inline run can ask you things. When it prints its first output it takes the keyboard, so you answer a `sudo` password prompt or a `[y/N]` by typing. The panel header reads "typing here" while your keys go to the program, and focus returns to the note when the command finishes.
If you pressed ⌘↩ and carried on writing, the run does not take the keyboard. Your caret moved on, so your typing stays in the note.
If the connection to the server drops, the panel stops taking your typing and the line reads "not connected" instead. It takes typing again as soon as the connection is back ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)).
To leave a running command, press Escape twice (the first Escape goes to the program, in case it wanted it) or ⌘Escape. While a full-screen program such as `vim` holds the panel, every Escape belongs to it, and ⌘Escape is the way out.
On a phone a run never takes the keyboard by itself, because taking it would raise one over half the screen you just asked to look at. While a run is going and your keys are still in the note, its header carries a Tap to type button, which is how you answer a password prompt or a `[y/N]` there. Tapping the output does the same thing.
On a touch device the header shows a Back to note button instead, for as long as the run holds the keyboard. Leaving does not stop the command: the run carries on, and tapping the output puts you back in it.
The bar above the keyboard changes while the run holds it. In place of the writing verbs it carries the keys a software keyboard has not got: `^C`, `^D`, Escape, and the four arrows. That is how you interrupt a command, end one that is reading input, quit a pager, or move around in `vim`, `less` or `htop` from a phone.
Back to note is the last button on that bar as well as in the panel's header, because a full screen program can push the header off the top of the screen.
Everywhere else that last button hides the keyboard, which on a phone is the only way to put one away: the note fills the screen, so there is no blank space to tap.
## Confirm before running
Add `confirm` after the language on the fence and Ledge asks before running the block:
````markdown
```sh confirm
rm -rf ./cache
```
````
Here is the whole exchange, on a harmless block:
```sh confirm
echo "this one asked first"
```
Running it opens a dialog showing the block's code with Cancel focused. Confirming runs the block, and its output streams in as usual:
```txt
this one asked first
```
The dialog shows the block's code, names where it is about to run, and opens with Cancel focused, so a stray Return does nothing. Nothing runs while the dialog is up. Cancelling remembers nothing, and the next ⌘↩ asks again. There is no "don't ask again".
Four ways to set it:
| Where | What it does |
| --------------------------------------------------- | -------------------------------------------------- |
| `confirm` on the fence | That block asks. |
| `confirm="Wipe the production cache?"` on the fence | That block asks, using your wording. |
| `confirm: true` in the note's frontmatter | Every runnable block in the note asks. |
| `confirm=no` on the fence | That block never asks, even under `confirm: true`. |
Use the custom message when the code alone does not say enough:
````markdown
```sh confirm="Wipe the production cache?"
redis-cli -n 0 flushdb
```
````
On a note that declares several machines ([Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts)) you pick the machine first, and the question names it. The last thing you read before running is which machine you are running on.
`confirm` lives in the fence's info string, which other Markdown renderers ignore. The block still highlights as `sh` on GitHub and in any editor, and the marker travels with the block when you copy it into another note.
This is a speedbump against muscle memory, not a lock. Anyone who can edit the note can delete the word.
## Keep a block from running
Add `norun` after the language and the block gets no Run button. ⌘↩ on it says so instead of running:
````markdown
```sh norun
sudo systemctl enable --now ledge-backup.timer
```
````
Use it for a command that belongs on some other machine, or in some other directory, than the note's shell: an install step for a server, a line for a project's terminal, a command you are quoting rather than keeping. The block still highlights as `sh`, and Copy still copies it.
`norun=no` turns it back off, the same way `confirm=no` does. Like `confirm`, the word lives in the fence's info string, so other renderers ignore it and it travels with the block.
Inside the app, the manual's own blocks are all marked this way, which is why none of them has a Run button.
## Shell blocks share one shell
Shell blocks (`sh`, `bash`, `zsh`) run in the note's persistent inline shell. There is one per note, so a `cd`, an exported variable, or an activated virtualenv carries into the next run.
```sh
count=$((${count:-0} + 1))
echo "run number $count"
```
The first run prints `run number 1` and the second `run number 2`, because both ran in the same shell.
The terminal drawer is a separate shell from the inline one. Both belong to their note alone, and both start where the note's frontmatter points them (`cwd`, `env`, and the rest: see [Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments)).
Comments mean the same thing on both chords. A `#` line inside a shell block is a comment whether you run the block inline or send it to the drawer, so you can annotate a block without breaking it.
Frontmatter applies to newly spawned shells, so after editing it run "Restart Note Shell" from the palette. It kills the note's shells and lets them respawn. Use the same command when an experiment leaves a shell in a strange state.
## Change the shell
Ledge spawns your own login shell with `-i` for every inline shell and every terminal drawer, as long as that shell is zsh or bash. Set `shell.path` and `shell.args` in Settings (⌘,) to use a different one:
```json
"shell": {
"path": "/opt/homebrew/bin/bash",
"args": ["-i"]
}
```
Relaunch to apply. Keep an interactive flag in `args`, usually `-i`, so your rc files run and blocks get the aliases and PATH you expect.
zsh and bash are the two shells Ledge can read block output from. It marks where a block's output starts and stops with a hook that only those two provide. Any other shell runs the terminal drawer normally, and its inline runs show no output and no exit code. Ledge warns about that in the launch log rather than overriding what you set.
A shell that is not installed refuses the run and names the path it could not find. Nothing quietly falls back to a different shell, because a different shell is not the one you asked for.
When the shell is zsh, Ledge spawns it with `-o interactive_comments` on top of your `args`. That is what makes a `#` line a comment in the terminal drawer, where the block is typed into the shell rather than sourced from a file. zsh leaves the option off by default, so without it the drawer answers a comment line with `command not found: #`. Put `+o interactive_comments` in `args` to keep zsh's own behavior.
This setting is about shells on the machine holding the notes, so a workspace on a remote server reads the copy on that server. A note with a `host:` line runs its blocks in the host's own shell instead ([Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts)).
## Interpreted languages
Languages with an interpreter mapping (`python`, `node`, `ruby`, `ts`, `php`, and others) run as a file handed to that interpreter, one fresh process per run. No state carries between runs.
```python
import platform
print(f"hello from Python {platform.python_version()}")
```
TypeScript uses the Bun runtime bundled with the app, so `ts` blocks run with nothing installed.
That runtime is the app's, not a server's. A note kept on a remote server runs its `ts` blocks on whatever `bun` the server has, and says `command not found` when it has none ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)).
## Redis and Valkey
A `redis` block is a list of commands, fed to `redis-cli` one line at a time.
```redis
PING
INFO server
```
With nothing configured it talks to a server on this machine. Set `REDIS_URL` in the note's frontmatter `env`, or in a profile when the URL carries a password ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)), and the same block points at staging instead. The target is a note-level fact, so two notes can hold the same commands aimed at different servers.
Valkey speaks the same protocol, so `redis-cli` drives a Valkey server too. If `valkey-cli` is the binary you have installed, name it in `blocks.interpreters` (below).
## Add a language
Settings (⌘,) holds both lists:
* `blocks.runnable` names the fence languages that get a Run button.
* `blocks.interpreters` maps a language to the command that runs it. Values may carry flags, such as `"python3 -u"`.
* `blocks.hostInterpreters` overrides interpreters per machine, for runs a note sends over ssh ([Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts)).
SQL is the common case. Add `"sql"` to `runnable` and this to `interpreters`:
```json
"sql": "psql \"$DATABASE_URL\" -f"
```
Relaunch, and `sql` fences run against whatever `DATABASE_URL` the note's frontmatter names, the same way `REDIS_URL` works above.
```sql
select count(*) from orders where created_at > now() - interval '1 day';
```
There is no default for `sql`, because the word does not say which engine you mean. Use `mysql`, `sqlite3 mydb.db <`, or `duckdb` to match yours.
Two things to expect from a database fence:
* Client tools page their output, so a wide result opens the pager inside the block and waits for you to quit it. You can type into a running block.
* A query with no `limit` prints every row it gets. Keep the limits you would keep in a terminal.
One more fence: a `prompt` block sends its text to an AI agent (Claude Code by default) with the note's context attached. See [Agents and Ledge](https://ledge.sh/docs/agents-and-ledge).
---
# Notes and Workspaces
Source: https://ledge.sh/docs/notes-and-workspaces
> This page covers where your notes live and how to arrange them: the file a note is, the folder a workspace is, the folders you file notes into, how a workspace reaches other people, and the tabs and panes you read them in.
This page covers where your notes live and how to arrange them: the file a note is, the folder a workspace is, the folders you file notes into, how a workspace reaches other people, and the tabs and panes you read them in.
## A note is a Markdown file
Press ⌘N and start typing. A new note opens as `# Untitled` with the word Untitled selected, so what you type becomes the title: type `Release Checklist` and Ledge names the file `release-checklist.md`. The first line names the note, so changing that heading later renames the file to follow. If two notes want the same name, the newer one gets a numbered suffix.
Notes are plain files, so anything can work on them: git, grep, scripts, other editors, agents. Ledge watches the folder and picks up outside changes as they happen, even while a note is open.
Ledge saves as you type, a moment after you pause. ⌘S saves at once.
## A workspace is a folder
The strip at the top of the sidebar lists your workspaces. Each one is a single folder of notes, and ⌘1 through ⌘9 jump between them. There are two kinds:
* **Attached.** "Attach Folder as Workspace…" (in the command palette, or the + button's menu) turns a folder you already have into a workspace, usually a project you work on. Its `.md` files become notes where they are, and each of those notes runs its blocks in the project folder with no frontmatter ([Running Code](https://ledge.sh/docs/running-code)).
* **Managed.** ⇧⌘N creates a workspace whose folder Ledge makes for you inside `~/.ledge`. Its notes default to your home folder, and `cwd:` frontmatter points them elsewhere ([Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments)).
Managed workspace folders live in `~/.ledge`.
With a workspace row focused, or from its right-click menu:
* Enter switches to it.
* `r` renames it, `i` changes its icon.
* Dragging reorders the strip.
* "Move Workspace Folder…" relocates the folder on disk.
* ⌫ closes it, which only detaches it. No files are touched, and attaching the same folder later brings everything back.
## Share a workspace with others
Put the workspace in a git repository and let the others clone it ([Tutorial: Share Notes with a Git Clone](https://ledge.sh/docs/tutorial-share-notes-with-a-git-clone)). A workspace is a folder, and each clone of that folder attaches as an ordinary workspace.
Everyone works in their own clone. Notes appear and change on screen when a pull lands, with nothing to refresh.
Git merges what you all wrote. If a pull rewrites a note you have open and edited, your version keeps the file and the incoming one goes to the workspace trash, with a notice in the sidebar naming it.
A clone carries the notes, their images, and their frontmatter. It does not carry your profiles, which live outside every notes folder ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)). Locked notes travel as ciphertext and arrive shut ([Note Locking](https://ledge.sh/docs/note-locking)).
Blocks run on the machine that opens the note. A note's `cwd:`, `host:`, and `profile:` lines name paths, machines, and credentials on the setup it was written for, so a block that deploys from your laptop may find none of that in anyone else's clone ([Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments)).
Read a workspace somebody sends you before you run anything in it, as you would a script from the same person.
Giving somebody a login to your Ledge server also gives them your notes, along with everything else on the machine ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)).
## Folders
Notes can sit in folders inside the workspace, and the sidebar shows them as a tree. Click a folder to open it, click again to close it. A closed folder shows how many notes are inside. With a folder's row focused, ↑ and ↓ walk the tree and Enter opens or closes it, the same as everywhere else in the sidebar.
Which folders you have open is part of the layout, so the tree comes back the way you left it at the next launch, per workspace ([Panes and Tabs](https://ledge.sh/docs/panes-and-tabs)). A folder that is gone by then comes back closed, since there is nothing left to open.
Press `/` with a folder's row focused, or choose "Search in Folder" from its right-click menu, to look only inside it ([Finding Things](https://ledge.sh/docs/finding-things)).
Press `r` with a folder's row focused, or choose "Rename Folder…" from its right-click menu, to rename it. The row turns into a text field: type the new name and press Enter, or press Escape to leave it alone. Every note in the folder, and in the folders inside it, comes along, and any of them you have open stay open.
The field takes a name, not a path, so a rename cannot move the folder somewhere else. Ledge refuses a name another folder here already answers to, rather than merging the two: to combine two folders, move the notes across. Locked notes are no obstacle, because a rename leaves their contents untouched.
Press `d` or ⌫ with a folder's row focused, or choose "Delete Folder…" from its right-click menu, to delete it. Ledge asks first, and says how many notes that is, because a closed folder does not show what is inside it. Every note in the folder goes to the Trash, including the ones in the folders inside it. "Undo" in the strip at the bottom of the sidebar brings them all back at once, and the Trash section can restore them one at a time later on.
Anything in the folder that is not a note stays where it is: an image you put there yourself, say, or a folder you told Ledge to ignore. The row goes either way, because Ledge only shows folders with notes in them.
There are three ways to put a note in a folder:
* **Drag its row** onto a folder. Dropping it on the Notes header at the top moves it back out to the workspace itself.
* **"Move to Folder…"**, from the note's right-click menu, from the command palette, or by pressing `m` with the row focused.
* **"New Note in Folder"**, from a folder's right-click menu, which starts a new note already in it.
"Move to Folder…" opens a list of the workspace's folders. Type to narrow it, and if what you type is not a folder yet, the last row offers to create it. A name with slashes in it, like `projects/api`, makes a folder inside a folder. Nothing is created until you pick a row, so Escape leaves no empty folder behind.
**"New Folder…"** is in the File menu, in the command palette, in the menu beside the New Note button, and in a folder's right-click menu, where it makes a folder inside that one. It creates the folder and opens the first note in it, because Ledge shows the folders its notes are in: a folder with nothing inside has no row.
That is also why moving the last note out of a folder takes the folder's row with it. The folder itself is still on disk, and putting a note back in it brings the row back.
Moving a note keeps everything about it. The file keeps its name, the tab stays open, and its images and links still work: Ledge rewrites the note's image references to point at the same pictures from where it now sits ([Images](https://ledge.sh/docs/images)). Wikilinks need no rewriting at all, because `[[Title]]` finds a note by its heading and not by its path ([Finding Things](https://ledge.sh/docs/finding-things)).
A locked note has to be unlocked before it can move, because those image references are inside the encrypted body ([Note Locking](https://ledge.sh/docs/note-locking)).
Two notes in different folders may share a title. Ledge shows the folder beside the title wherever the list is flat: quick-open, full text search, backlinks, and tag results.
## Favorites
A favorite note sits in a Favorites section at the top of the sidebar, above the tree, however deep in a folder it actually lives.
Favorite a note by pressing `f` with its row focused, by choosing "Favorite" from its right-click menu, or by clicking the star that appears on the row when you point at it. The same three ways unfavorite it, and the menu item says "Unfavorite" once the note is marked.
The note stays where it is. Its row in the tree does not move, because the tree says where a note lives and the section says which notes you keep coming back to. A favorite has a row in both places, and either row's `f`, star or menu unfavorites it.
Favorites are per workspace, and the section is not there at all until you mark something.
Marking a note writes `favorite: true` into its frontmatter ([Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments)), so it is part of the note: it survives a rename, travels when you move the note to another folder, and comes across with the file if you sync your notes to another machine. You can type the line yourself instead, and take it out by deleting the line.
A locked note can be favorited without unlocking it, because the marker sits in the part of the file that stays readable ([Note Locking](https://ledge.sh/docs/note-locking)).
## Which files become notes
Every `.md` file in the workspace folder and its subfolders is a note, with two kinds of exception.
Dot-prefixed files and folders are skipped, which keeps `.git` and Ledge's own `.ledge-assets` and `.ledge-trash` out of the list. So are the usual vendor and build directories, at any depth: `node_modules`, `bower_components`, `vendor`, `dist`, `build`, `out`, `target`, `coverage`, `__pycache__`, `Pods`, and `DerivedData`.
A `.ledgeignore` file in the workspace folder adds your own, one pattern per line, in a small subset of gitignore's grammar:
| Line | Skips |
| -------------- | ------------------------------------------------------------------ |
| `drafts` | Any file or folder named `drafts`, at any depth. |
| `drafts/` | Only a folder of that name. |
| `docs/archive` | That path, counted from the workspace folder. |
| `*.wip.md` | Names matching the glob. `*` and `?` stay within one path segment. |
| `!build` | Nothing. It brings `build` back, and the last matching line wins. |
| `# text` | Nothing. A comment. |
Ignoring only hides. An ignored note is absent from the sidebar and from search, and a note you had open when it became ignored still saves.
## Live preview
Ledge hides Markdown syntax away from the cursor. `**bold**` shows as bold and the asterisks come back when you move the cursor into it, a link shows its label, a checkbox is clickable, and tables and images render in place. Text in backticks is drawn on a tinted chip, so it still reads as code once the backticks themselves are hidden.
Set `editor.livePreview` to `false` under This app in Settings (⌘,) and relaunch to see every character all the time. Tables and images stay as text in that mode too. Use it when you are editing syntax precisely and want the text on screen to match the text on disk.
Everything else is unaffected: ⌘B, the `[[` picker, and fence completion work the same either way.
## Right-clicking in a note
Right-click anywhere in a note to get a menu of what you can do there.
Cut, Copy, Paste, Paste as Plain Text and Select All are always in it, followed by Bold, Italic, Insert Link, Link to Note, Code Block and Insert Image.
Above those sits whatever you clicked on: Open Link on a link, a `[[wikilink]]` or a `#tag`, Toggle Checkbox on a task, and both run verbs inside a runnable code block.
The click moves the cursor to where you clicked, so the menu acts on that spot.
Right-clicking inside a selection keeps the selection, which is how you cut, copy or bold the text you just selected.
Every item shows its keyboard shortcut beside it, so the menu is also where you find them.
## Pasting formatted text
⌘V converts formatted text to Markdown. Copy a section of a web page, an email, a Slack thread, or a Google Doc, and the structure survives the paste:
| Copied | Pasted |
| --------------------------- | ----------------------------------------------------------- |
| Heading | `## Heading` |
| Bold, italic, strikethrough | `**bold**`, `*italic*`, `~~struck~~` |
| Bulleted and numbered lists | `- item`, `1. item`, nested and indented |
| Checkboxes | `- [x] done` |
| Link | `[label](https://example.com)` |
| Table | A GFM pipe table, alignment included |
| Code block | A fence, labelled with the language when the page named one |
| Quote | `> quoted` |
| Image on the web | `` |
⇧⌘V pastes the text as it is, with no conversion. Use it when you want the words and none of the markup.
Ledge converts only what carries formatting. Copying from a terminal, an editor, or the browser's developer tools puts styled but unstructured HTML on the pasteboard, and pasting that gives you your lines exactly as they were. A paste inside a fenced block, a code span, or a frontmatter block is never converted: the text there has to be exact.
An image on the pasteboard is embedded as a file instead. See [Images](https://ledge.sh/docs/images).
## Deleting a note
`d` or ⌫ on a note's row, or ⌘⌫ from the editor, moves the note to the workspace's trash and shows an Undo strip for a few seconds.
Nothing is lost when the strip fades. The Trash section at the bottom of the sidebar holds the note, where `r` restores it and `d` deletes it permanently after a confirmation. "Empty Trash…" in the command palette does that for every note in it. Trashed notes are purged after 30 days, set by `trash.ttlDays`.
The trash mirrors your folders, so restoring a note puts it back in the folder it was deleted from, creating that folder again if it has gone.
## Tabs and panes
Notes open in tabs, and ⌘D splits the view so two notes sit side by side. Each workspace keeps its own arrangement.
See [Panes and Tabs](https://ledge.sh/docs/panes-and-tabs) for splitting, moving tabs between panes, and what Ledge restores at launch.
---
# Panes and Tabs
Source: https://ledge.sh/docs/panes-and-tabs
> A workspace's editing area is a tree of panes, and each pane holds its own tabs.
A workspace's editing area is a tree of panes, and each pane holds its own tabs. This page covers splitting, moving tabs between panes, and what Ledge saves about the arrangement.
Panes put notes side by side. Each note keeps its own shell ([Running Code](https://ledge.sh/docs/running-code)), so two panes are two working environments rather than two views of one.
## Split a pane
⌘D splits the focused pane left and right. ⇧⌘D splits it top and bottom.
The new pane opens with a fresh Untitled note and takes focus. Either side can split again, along either axis, as many times as you like. The layout is a tree, not a fixed two-column or three-column arrangement.
(Illustration: ⌘D splits the view. The new right pane opens a fresh note and takes focus, the left pane dims but keeps its note, and each pane carries its own tab strip. A second note then opens as a tab in the focused pane.)
A tab's context menu carries Split Right, Split Down and Close Pane too, and those act on the tab's own pane rather than the focused one.
On a phone the tab strip has no split buttons, because two panes on a screen this narrow are two editors about 195 points wide. The commands are still in the palette and in a tab's menu, so a split you ask for by name is a split you get.
## Focus a pane
Click anywhere in a pane to focus it. The caret moves to that pane's editor, and the other panes dim.
Focus decides what most commands act on. ⌘W closes a tab in the focused pane, ⌃\` opens the focused note's terminal, and a note you open from ⌘P lands there.
There is no chord for moving focus between panes. Click to change panes, and use ⌃Tab to move within one.
## Resize a split
Drag the divider between two panes.
A pane can take between 12% and 88% of the space its split divides. The ratio is saved with the layout.
## Close a pane
⇧⌘W closes the focused pane, and its space returns to the neighboring pane.
The ✕ at the end of a pane's tab strip does the same for that pane, and a tab's context menu carries Close Pane too. Both appear only once a workspace has more than one pane, so the single-pane arrangement shows nothing to close.
The last remaining pane in a workspace does not close.
Closing a pane's tabs one at a time leaves the pane standing and empty, showing a New Note button. ⇧⌘W is what removes the pane itself.
On a phone the ✕ stays, unlike the two split buttons beside it. There is no ⇧⌘W to press there, so the ✕ and the menu item are the two ways out of a split.
## Tabs in a pane
Each pane has its own tab strip.
| Key | Action |
| -------------- | ------------------------------------------------ |
| ⌘N | New note in the focused pane. |
| ⌘W | Close the active tab. |
| ⌃Tab and ⌃⇧Tab | Next and previous tab. ⇧⌘] and ⇧⌘\[ do the same. |
| ⌃1 to ⌃9 | Jump to a tab by position. |
Hold ⌃ and each tab shows its number. The + button at the end of the strip is New Note, and a tab's context menu holds Close Tab, Close Other Tabs, and Keep Tab Open.
When a pane holds more tabs than fit, the strip scrolls sideways and fades at whichever edge is hiding tabs.
## Preview tabs
A note you open by navigating to it opens in *italics*. That tab is a preview: the next note you navigate to takes its place instead of adding a tab beside it.
Clicking down the sidebar, or through a run of search hits, therefore leaves you with one tab rather than a strip of notes you looked at once.
A preview tab becomes a permanent one, and stops being replaced, as soon as you do any of these:
| What keeps it | Where |
| -------------------- | ------------------------------------------------------------------ |
| Type in the note | The usual one. A note you are editing is a note you meant to keep. |
| Double-click its tab | In the strip. |
| Double-click its row | In the sidebar, on the row you opened the note from. |
| Drag its tab | A tab you put somewhere stays there. |
| Keep Tab Open | The tab's context menu, the View menu, and the command palette. |
New notes are never previews. ⌘N, New Note in Folder, a note made from a template, and `ledge ` from a terminal all open a tab that stays.
Nothing you have typed can be swept away by this. Typing in a note is itself what keeps its tab, so a tab that gets replaced is always one holding exactly what is on disk.
On a phone the behavior is the same, and Keep Tab Open is in the menu a long press on the tab opens ([Ledge on Your Phone](https://ledge.sh/docs/ledge-on-your-phone)).
## Move a tab between panes
Drag a tab onto another pane's strip and drop it where you want it in the order. Dragging within one strip reorders it.
A moved tab keeps its caret, scroll position, undo history, and the output of any block still running in it. Ledge re-parents the editor rather than rebuilding it, both when a tab moves and when you switch tabs.
The destination pane takes focus, as if you had clicked the tab there. If the tab was active in the pane it left, that pane falls to the tab beside it.
## Where a note opens
A note that is already open focuses its existing tab, wherever that tab lives, including in another workspace. Ledge never opens one note twice, because two tabs on one file would be two editors saving over each other.
A note that is not open becomes a new tab in the focused pane. This is how ⌘P, search hits, wikilinks, backlinks, and `ledge ` from a terminal all arrive ([Finding Things](https://ledge.sh/docs/finding-things), [The ledge CLI](https://ledge.sh/docs/the-ledge-cli)).
All of those except `ledge ` are navigations, so the tab they open is a preview one.
## The terminal drawer follows the focused note
⌃\` opens the terminal for whichever note has focus. Switching panes or tabs swaps the drawer to that note's shell.
Shells keep running while the drawer shows another note's. Coming back replays the scrollback.
There is one drawer, spanning the window below the panes, rather than one per pane.
## Layouts are saved per workspace
Each workspace keeps its own pane tree. ⌘1 through ⌘9 switch the whole arrangement, not just the note ([Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces)).
Ledge saves the layout as you change it and restores it at the next launch. The sidebar's open folders are saved with it, so the tree comes back the way you left it too ([Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces)).
Two things do not come back:
* **Tabs you never typed in.** A note has no file until its first edit, so an untouched Untitled tab has nothing to restore. Its pane returns with a fresh one.
* **Notes that moved or were deleted while Ledge was closed.** Those tabs are dropped and the rest of the layout restores around them.
A preview tab comes back a preview one, so the strip returns exactly as you left it, down to which tab the next note you open will replace.
## The window
Ledge reopens the windows you left open, each at the position and size you left it.
If a position no longer exists, because you unplugged the display it was on or the display got smaller, Ledge keeps the size and centers the window on the display that best matches.
New Window in the File menu opens another one. A window is on one server at a time, so a second window is how you have two servers open at once ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)).
The help button in the top right opens the manual in a window of its own, so reading it costs you nothing you had open. Pressing it again brings that window forward rather than opening a second one. It is the one window Ledge does not reopen at the next launch, since it is a button away.
Two more keys for the window itself:
* ⌥⌘B hides the sidebar.
* ⌃⌘F enters full screen, also in the View menu.
---
# Finding Things
Source: https://ledge.sh/docs/finding-things
> Four ways back to what you wrote: quick open, full-text search, links, and tags.
Four ways back to what you wrote: quick open, full-text search, links, and tags. All of them are scoped to the selected workspace.
## Quick open, search, and the command palette
⌘P opens a note by title, fuzzy matched. From there the first character you type switches modes: `#` for full-text search, `>` for the command palette. Two chords land in a mode directly: ⌥⌘P for search, ⇧⌘P for commands.
Three chips under the field do the same thing without the punctuation: Notes, Commands, Text. The one you are in is lit, and switching keeps what you have typed, so a title that turns up nothing becomes a text search in one tap.
The magnifier in the header opens the same thing, so all three modes are one click away when you would rather not reach for a chord.
When a title search matches nothing, the list offers to search the text of every note for the same words. Tap it, or press Enter.
A search hit opens the note with the matched line revealed and selected, so you land on the words rather than at the top of the file.
(Illustration: the quick open panel. Typing `ship` narrows the note list to matching titles. Retyping the query as `#flag` switches to full-text search, and each hit shows the note title over its matched line.)
Rows name the folder a note is in, beside its title, so two notes with the same title in different folders are told apart ([Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces)). A note at the top of the workspace shows no folder.
## Search inside one folder
Right-click a folder in the sidebar and choose "Search in Folder", or press `/` with its row focused. The overlay opens with that folder's name as a pill beside the field, and every row below comes from it and the folders inside it.
The pill stays put as you switch modes, so the Notes chip lists that folder's notes by title and the Text chip searches their bodies. Click the pill, or press Backspace with the field empty, to widen back out to the whole workspace.
The Commands chip hides the pill while you are on it: commands are not in a folder. Switching back brings it and its rows back together.
A `#` query lists that folder's tags rather than the workspace's. Enter on a tag row still opens the Tags panel, which is always the whole workspace.
This is the same narrowing that `ledge search -f` and the agent tools' `folder` argument give you outside the app ([The ledge CLI](https://ledge.sh/docs/the-ledge-cli), [Agents and Ledge](https://ledge.sh/docs/agents-and-ledge)).
## Wikilinks
`[[Note Title]]` links to a note by its title, and `[[Note Title#Heading]]` targets a heading inside it. Typing `[[` opens a picker over the workspace's notes.
A wikilink names a title, never a path, so moving a note into a folder breaks none of the links to it.
Titles are matched exactly, ignoring case. If two notes in different folders share a title, the link opens the one edited most recently, so give notes you link to titles of their own.
Links address the title, never the filename. A retitle renames the file without leaving a stale path behind, and nothing rewrites your other notes to keep links working. A link whose title matches nothing is styled as dangling and edits like plain text.
Click a rendered link to follow it. When the caret is inside a link and the raw text shows, ⌘-click instead.
## Backlinks and outline
⌥⌘L opens the Backlinks panel: every note that links to the current one. Enter opens the linking note at its link line.
⌥⌘O opens the Outline panel: the current note's headings, updated as you type. Enter jumps the caret to a heading, and `c` copies that heading's wikilink for pasting into another note.
The panels share the right-hand slot, so opening one closes the other.
## Tags
Tag a note with inline `#hashtags` anywhere in the body, or with a frontmatter `tags:` line.
The `tags:` line takes one list on one line, separated by commas or spaces, with or without square brackets. `tags: ops, runbook` and `tags: [ops, runbook]` declare the same two tags. A leading `#` on an entry is allowed and comes off, so you can spell tags the way the body does. The indented `- ops` form is not read, so keep the list on the `tags:` line.
A tag is letters, digits, `_`, `-` and `/`, and needs at least one letter or `_`. That is why `#2024` and `#123` stay plain text: a year and an issue number are not tags. An entry Ledge cannot read as a tag is named beside the line, and the entries beside it still count. See [Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments) for the rest of the block.
⌥⌘T opens the Tags panel: every tag in the workspace with its count, where Enter drills into the notes bearing one. Rendered tags in the editor are clickable, typing `#` in a note completes against the workspace's existing tags, and a query starting with `#` in the search overlay lists matching tags as rows.
## Find and replace in a note
⌘F finds within the current note, ⇧⌘F finds and replaces, and ⌘G and ⇧⌘G step through the matches.
The panel opens above the note with its field focused. The arrows step through the matches, All selects all of them at once, and the three small toggles are match case, regular expression, and whole word. The chevron at the left end opens the replace row, where Replace rewrites the next match and Replace All rewrites every one.
Escape closes the panel, and so does the × at its top right.
On a phone the panel takes two rows: the field and the × on the first, the arrows and the toggles on the second. There is no Escape key to press there, so the × is the way out.
---
# Frontmatter and Environments
Source: https://ledge.sh/docs/frontmatter-and-environments
> A note can declare how its shells spawn: which directory they start in, which environment variables they carry, and which machine they run on.
A note can declare how its shells spawn: which directory they start in, which environment variables they carry, and which machine they run on. Those declarations live in a frontmatter block at the top of the note.
This page covers the block itself and the directory and environment keys. Secrets have their own page ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)), as does running on other machines ([Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts)).
## The frontmatter block
Frontmatter sits at the very top of a note: a `---` line, your `key: value` lines, then a closing `---` line. Press ⌥⌘, on any note to jump into its block, or to create one if the note has none. Completion inside the block offers every key with a one-line hint at the start of a line.
```
---
cwd: ~/Projects/my-app
envFile: .env
env:
NODE_ENV: development
PORT: "3000"
---
```
The grammar is small: flat `key: value` lines, plus one indented map under `env:`. Full-line `#` comments and blank lines are allowed, and a value with spaces can be quoted, as in `cwd: "~/My Notes"`.
A bad line costs only itself, never the rest of the block. Ledge writes what it could not read beside the line, in red, and marks that line down its left edge: an unknown key says so rather than doing nothing quietly. The note is untouched and the message goes as soon as the line is right, so you can keep typing through it.
## cwd: where shells start
`cwd:` sets the working directory for every shell the note spawns, both the inline shell and the terminal drawer. `~` expands to your home folder, and a relative path resolves against it. If the directory does not exist, the shell spawns in your home folder instead.
Notes in an attached project workspace use that project folder as their `cwd`, so they usually need no frontmatter. Notes in a managed workspace default to your home folder.
Inside a block, `$PWD` is that directory.
## env and envFile: environment variables
Three keys feed the environment, layered in this order, with later layers overriding earlier ones:
1. `envFile:` names a dotenv file, resolved against the note's `cwd`. So `envFile: .env` picks up the project's own env file when `cwd` points at the project. A missing file is skipped and the shell spawns anyway.
2. The note's profile, if it names one ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)).
3. `env:` sets values inline in the note.
`env:` is for plain, non-secret values. They sit in the note in the open, which suits `NODE_ENV` and does not suit an API key. Secrets belong in a profile: a named env file kept outside your notes folder, so syncing or sharing notes never carries credentials.
`TERM` is protected. A layer that sets it is overridden back, because the built-in terminal is the terminal whatever the note claims.
## When changes apply
Frontmatter is read when a shell spawns, and a note's running shells keep the settings they started with.
After editing the block, run "Restart Note Shell" from the command palette (⇧⌘P). It kills the note's shells, and the next run or drawer visit respawns them with the current frontmatter. Use the same command when an experiment leaves a shell in a strange state.
## Every key
| Key | What it does |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cwd:` | Working directory for the note's shells. |
| `env:` | Environment variables, set inline. |
| `envFile:` | Dotenv file to load, resolved against `cwd`. |
| `profile:` | Named env file outside the notes folder, for secrets. See [Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets). |
| `host:` | Machines the note's blocks run on. See [Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts). |
| `tags:` | Tags, the same vocabulary as inline `#hashtags`. See [Finding Things](https://ledge.sh/docs/finding-things). |
| `template:` | `true` lists the note in the New Note from Template picker; `daily` makes it the daily template. See [Daily Notes and Templates](https://ledge.sh/docs/daily-notes-and-templates). |
| `favorite:` | `true` keeps the note in the Favorites section at the top of the note list. Usually written for you by the Favorite command. See [Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces). |
| `confirm:` | `true` makes every runnable block in the note ask before running. A single block opts out with `confirm=no` on its fence. See [Running Code](https://ledge.sh/docs/running-code). |
| `locked:` | Written by "Lock This Note…" to mark an encrypted note. You never type it. See [Note Locking](https://ledge.sh/docs/note-locking). |
---
# Profiles and Secrets
Source: https://ledge.sh/docs/profiles-and-secrets
> A profile is a named file of environment variables that lives outside your notes folder and is injected into the shells of any note that names it.
A profile is a named file of environment variables that lives outside your notes folder and is injected into the shells of any note that names it.
Use one for secrets. Notes get synced, backed up, shared, and read by agents, so an API key written in an `env:` line travels everywhere the note does. With a profile, the note carries only a name.
## Declare a profile
Add one line of frontmatter (see [Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments) for the block itself):
```
---
profile: deploy
---
```
Profile names may contain letters, digits, `-`, and `_`. The name resolves to a file under `~/.config/ledge/profiles/`, here `deploy.env`, created for you the first time you open it for editing.
A note names at most one profile, and any number of notes can share one. Every deploy-related note can say `profile: deploy` and pick up the same credentials.
## Edit a profile
Click the profile name in the frontmatter block, or run "Edit Note Profile…" from the command palette. The command appears whenever the current note names a profile.
On a touch device the palette command is the whole of it. The small key button beside the name is a pointer control and is not drawn there, and the command asks for nothing to be pointed at: it follows the note you are in.
Either way you get Ledge's profile editor: KEY=value rows with the values masked, and one deliberate toggle to see them.
(Illustration: the profile editor for a profile named `deploy`, with `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` rows whose values are masked, a Show values toggle that reveals them, and Cancel and Save buttons.)
On disk the profile is a plain dotenv file: `KEY=value` per line, `#` comments, and an optional `export ` prefix. Ledge creates it readable only by you. Hand edits and editor edits coexist, and saves from the editor preserve your comments.
```
# deploy.env
API_TOKEN=abc123
DEPLOY_REGION=eu-west-1
```
## How profiles layer
Profile variables merge into the shell environment at spawn, above the note's `envFile` and below its inline `env:` lines. An `env:` line can therefore override a profile value for one note without editing the shared file.
A `profile:` line naming a file that does not exist is skipped, and the shell spawns without it.
A profile edit applies to newly spawned shells, like every frontmatter change. Run "Restart Note Shell" after changing one.
## Profiles stay on this machine
When a note runs its blocks on a remote host over ssh, Ledge does not send the profile ([Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts)). A secret passed on a remote command line would be visible in that machine's process table to anyone who can list processes. If a remote run needs credentials, put them on the remote machine.
---
# Run Code on Remote Hosts
Source: https://ledge.sh/docs/run-code-on-remote-hosts
> A host: line in a note's frontmatter sends every run, inline and in the terminal drawer, over ssh to the machine it names.
A `host:` line in a note's frontmatter sends every run, inline and in the terminal drawer, over ssh to the machine it names. The note stays where it is, and only the commands travel.
Ledge has two ways to involve a remote machine, and they answer different questions.
| | The note | Its blocks |
| ------------------------------------------------------------------------------------------ | ------------------- | --------------------------------------- |
| A `host:` line in the note ([Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts)) | Stays where it is | Run over ssh on the host it names |
| A server connection ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)) | Lives on the server | Run on the server |
| Both | Lives on the server | Run on the host, which the server dials |
This page is the first row. [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server) is the second, and the two compose: a note kept on a server can carry a `host:` line, and the server makes that ssh connection.
## Declare hosts
`host:` takes one or more ssh destinations on a single line, separated by spaces or commas. A destination is anything ssh accepts: `user@machine`, a bare hostname, or an alias from your `~/.ssh/config`. The reserved word `local` means this machine.
```
---
host: deploy@prod
---
```
With one host declared, every run goes there.
```
---
host: staging, deploy@prod, local
---
```
With more than one, Ledge asks on every run. A "Run on" menu appears at the block with your last pick focused, so Enter repeats it and a different machine takes an arrow key first. Ledge asks every time rather than remembering a default, because prod and staging sit next to each other in the same note.
```yaml
---
host: staging, deploy@prod
---
```
```sh
uptime
```
Running the block opens the Run on menu. Picking `staging` runs it over ssh, and the output header names the host:
```txt
14:02 up 41 days, 3 users, load averages: 0.32 0.28 0.25
```
## Authentication
Ledge runs your own `ssh` on a real terminal. Keys, agents, and everything in `~/.ssh/config` work as they do in any terminal, and passphrase prompts, host-key confirmations, and 2FA challenges appear where you answer them directly.
In the terminal drawer they appear as they come. Inline runs hold them for a few seconds first, because a healthy connection has started the block by then and its own output is what you want to see. So the first run against a new machine pauses, then shows you ssh asking whether to trust the host key. Answer it in the output panel and the block carries on.
Ledge does not manage connections. If they feel slow to start, use `ControlMaster` in your ssh config for connection reuse.
## What travels to the host
Two things, and no more:
* The note's `cwd` becomes a `cd` on the far side. `~` means the remote home folder, and a missing directory falls back to the remote home with a message.
* The inline `env:` lines are exported there.
`profile:` and `envFile:` stay local and are skipped with a warning. A secret sent along an ssh command line would sit in the host's process table for anyone to read ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)). If a remote run needs configuration, put it on the host.
## What runs on the host
Shell blocks from inline runs land in a `bash -l` login shell, so bash must exist on the host. The terminal drawer gets your own remote login shell, with your prompt and rc files intact.
Interpreted blocks (`python`, `node`, and others, per [Running Code](https://ledge.sh/docs/running-code)) work remotely too. The block's body travels with the run, and the interpreter is resolved from the host's PATH.
Two differences from local runs:
* `ts` blocks use the host's own `bun`, not the one bundled with the app, so the host needs bun installed for TypeScript.
* When a machine needs a different interpreter command than your default, such as `python3.11` instead of `python3`, set the override in `blocks.hostInterpreters` in Settings (⌘,).
---
# Keep Notes on a Remote Server
Source: https://ledge.sh/docs/keep-notes-on-a-remote-server
> Ledge can keep your notes on a server and run the app as the window onto it.
Ledge can keep your notes on a server and run the app as the window onto it. The server holds the notes, spawns the shells, and keeps them running; the app draws them. The transport is ssh, so there is no account to make and no service to sign up for.
One server at a time per window. The connection bar above the workspace strip always names the one you are typing into, and a server can serve several of your devices at once.
Ledge has two ways to involve a remote machine, and they answer different questions.
| | The note | Its blocks |
| ------------------------------------------------------------------------------------------ | ------------------- | --------------------------------------- |
| A `host:` line in the note ([Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts)) | Stays where it is | Run over ssh on the host it names |
| A server connection ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)) | Lives on the server | Run on the server |
| Both | Lives on the server | Run on the host, which the server dials |
This page is the second row. [Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts) is the first, and the two compose: a note kept on your VPS can carry `host: prod`, and the VPS makes that outbound ssh connection, so the app never holds credentials for prod.
## Add a server
Click the connection bar, or run "Notes On…" from the palette or the File menu. Choose Add, then fill in the fields.
| Field | What it takes |
| --------------- | ------------------------------------------------------------------ |
| Name | Anything you want to see in the bar. |
| SSH destination | `user@host`, a bare hostname, or a name from your `~/.ssh/config`. |
| Port | Blank unless sshd listens somewhere other than 22. |
| Sign in with | A key or a password. |
| Key | A private key to offer, or blank to let your ssh config decide. |
The port goes in its own field, not in the address: write `ledge@vps`, not `ledge@vps:2222`.
Leave it blank whenever you can. Blank means your ssh config decides, so an alias from `~/.ssh/config` keeps whatever `Port` it already sets.
Ledge then fetches that machine's host key and shows you its fingerprint. Compare it against what the machine reports for itself, in a shell on that machine:
```sh
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
```
Choose "It Matches, Add" only when the two agree. Ledge pins the key and refuses any future connection that presents a different one. There is no "connect anyway".
The pinned keys live in `~/.ledge/.client/known_hosts`, separate from your own `~/.ssh/known_hosts` so you can read and revoke them on their own. Your existing entries still work: a host you already trust needs no second pin.
A pinned key belongs to one address and one port. Change either and Ledge asks for the fingerprint again, because two sshd instances on one machine can offer different keys.
Add as many as you like. The list is the app's own and lives on this machine, so nothing about it is stored on any server.
## Edit or remove a server
Every row in the picker carries two controls: a pencil to change it and a bin to remove it. Press ⌫ on a focused row to remove it without reaching for either.
Editing opens the same form. A rename, or a change of account on the same machine, saves in one step, because neither changes which machine the pinned key belongs to.
Changing the address to a different machine does not. The button reads "Continue" instead of "Save", Ledge asks that machine for its host key, and you compare the fingerprint again before anything is stored. A key pinned for one machine says nothing about another, and carrying it across would refuse every later connection with a warning about a changed host key.
Use "Check Key Again" when a server you already have has legitimately rotated its host key. It is the same fingerprint step, on a connection you keep.
This Mac cannot be removed or edited, and neither can the connection you are currently using: switch somewhere else first.
## Sign in with a password
Choose "A password" in the form and type the password for that account on that machine. Ledge stores it and offers it on every connection, including the reconnects it makes by itself.
Use it when the machine has no key on it yet. A fresh VPS with a password is a machine you can reach today, and setting up a key afterwards is a change you make once. Keys are the better long-term answer, and switching a connection over to one later is one edit.
Ledge keeps the password in your Mac's keychain and never in `~/.ledge`. When ssh asks for it, ssh reads it from the keychain itself, so the password does not pass through Ledge on its way out.
Anything running as you on this Mac can read that keychain item. That is the same reach a private key file in `~/.ssh` gives, so a password here is neither safer nor less safe than the key it stands in for.
Removing the connection removes the password with it. So does switching that connection back to a key.
The password field is blank when you edit an existing connection, and blank means keep the one that is stored. Type a new one only when you want to replace it. If the new one does not work, Ledge puts the old one back and tells you the connection could not be reached.
Some servers do not allow passwords at all. If the machine reports "Permission denied", check `PasswordAuthentication` in its `/etc/ssh/sshd_config` before checking what you typed.
Restricting a key to Ledge does not apply to a password. That restriction is a line in `authorized_keys`, which is a file about keys.
## Switch servers
The picker opens on the connection in use, so Enter means stay and moving somewhere else takes an arrow key first.
Switching closes every tab and opens that machine's instead. Nothing is lost: the tabs are on the other machine and come back when you switch back.
A connection that will not open costs you nothing. Ledge reaches the new machine before it lets go of the old one, so a typo or a sleeping laptop leaves you exactly where you were with the reason on screen. If the failure happens at launch, Ledge opens on this Mac and the bar reads "not reachable".
## Two machines at once
New Window in the File menu opens a second window, and each window is on its own machine. Switching moves one window; a second window is how you have a build box and a VPS open side by side.
A new window opens on this Mac. Switch it wherever you like from inside it.
Each window is titled after the machine it is on, so the title bar reads "This Mac" or the name you gave the connection. That is the name in the Window menu too, and on a window's tab when macOS merges your windows into tabs.
The manual's window is the exception, titled "Documentation". It reads the copy of the manual that ships with the app, so it stays on this Mac whichever machine the window you opened it from is on.
Each window keeps its own tabs and panes, and the server remembers them: switch a window back to a machine you used before and its arrangement comes back. Ledge reopens every window you left open at the next launch, each on the machine it was pointed at.
Two windows on the same machine are the exception. Only one of them can be that machine's arrangement, so the second opens empty and does not overwrite the first.
Closing the last window quits Ledge.
## Install the server
The other machine needs `ledge-server` on the PATH an incoming ssh gets. It is a package, so two commands install it. [Tutorial: Set Up a Ledge Server](https://ledge.sh/docs/tutorial-set-up-a-ledge-server) walks through them on a fresh VPS, with an account for Ledge and the sshd hardening this page describes further down.
The server runs on Bun, and where Bun goes decides where the server goes, because Bun puts global commands beside itself. On that machine, install Bun into `/usr/local` and both names land in `/usr/local/bin`, which is where the short PATH of an ssh command looks:
```sh
curl -fsSL https://bun.sh/install | sudo BUN_INSTALL=/usr/local bash
```
Then the server, into the same place:
```sh
sudo BUN_INSTALL=/usr/local bun add -g ledge-server
```
The variable on the second command is not optional. Without it the package installs into the home directory of whoever ran it, which is not a directory an incoming ssh searches.
macOS and Linux are supported, on arm64 or x64. On Linux the floor is glibc 2.29, which means Debian 11, Ubuntu 20.04, RHEL 9, or anything newer. Alpine and other musl systems are not supported.
Nothing else has to be installed and no port is opened. Ledge speaks its protocol over ssh's stdin and stdout.
Blocks need zsh or bash on that machine. Ledge spawns the account's login shell when it is one of those, and otherwise the first of the two it finds, so an ordinary Linux install needs nothing extra. Where neither exists, a run refuses and names the shell it looked for instead of appearing to do nothing.
## Check that ssh can find the server
Worth doing once, because Ledge reports the failure it catches as a server that is not installed. A remote shell that cannot find a command says only that, so that is all the app has to go on.
Ledge starts the server by running `ledge-server serve` over ssh. A command run that way gets a short PATH and reads no shell profile, so both `ledge-server` and the `bun` its first line names have to be on that PATH already. From your Mac's own terminal:
```sh
ssh you@machine 'command -v ledge-server; command -v bun'
```
Two paths printed means the machine is ready to add.
Nothing printed means Bun is installed for one user rather than system-wide, which is what a machine that already had Bun before you started usually has. Its global commands are then in `~/.bun/bin`, which an incoming ssh does not search, and `bun pm bin -g` on that machine confirms where they went. Linking both names into a system directory, on that machine, fixes it without reinstalling anything:
```sh
sudo ln -s "$(bun pm bin -g)/ledge-server" /usr/local/bin/ledge-server
sudo ln -s "$(command -v bun)" /usr/local/bin/bun
```
## Build the server from a checkout
Only if you want a build of your own. The package is the ordinary way. From a checkout of the repository:
```sh
bun run build:native
bun build src/bun/serve.ts --compile --outfile ledge-server
```
Copy `ledge-server` and `dist-native/libledge_pty.so` to the server, side by side, somewhere on the PATH. Build it on a machine of the same architecture as the one that will run it, since a compiled binary is one architecture and the package is the thing that carries all of them.
The `.so` holds two C functions the terminal needs. Without it beside the binary, resizing a terminal does nothing and a shell that stops reading can stall the server.
## Run the server in Docker
The repository ships a `Dockerfile`. Build and run it from a checkout on the machine that will host the container:
```sh
docker build -t ledge-server .
docker run -d --name ledge --restart unless-stopped -v ledge-data:/data -v ledge-home:/home/ledge ledge-server
```
Mount both volumes. `/data` holds the notes, the workspace registry, the vault, and the logs. `/home/ledge` holds the account: your profiles and their secrets live in `~/.config/ledge/profiles`, and the keys `host:` frontmatter dials out with live in `~/.ssh`.
Neither directory is the whole story on its own, and `docker rm` takes whatever you did not mount. Run `ledge-server backup-paths` in the container to see both, resolved. "Back up the server" below is what to do with them.
The image has no ssh daemon in it. The machine's own sshd is the one that answers, and it reaches into the container (see below). Running a second sshd inside a container means a second set of host keys and a second published port, for nothing.
The image carries zsh, `ssh`, and nothing else your notes might want. Add what you need in an image of your own:
```dockerfile
FROM ledge-server
USER root
RUN apt-get update && apt-get install -y --no-install-recommends git python3
COPY --from=oven/bun:1-debian /usr/local/bin/bun /usr/local/bin/bun
USER ledge
```
The `bun` line is there for `ts` blocks. The app carries its own copy of that runtime and a server carries none ([Running Code](https://ledge.sh/docs/running-code)).
## Restrict the key to Ledge
Optional, and worth doing on a server you care about. Ledge connects with an ordinary key either way and never edits this file for you.
Restricting gives the server a key that can speak Ledge's protocol and nothing else. In that machine's `~/.ssh/authorized_keys`:
```
restrict,command="/usr/local/bin/ledge-server serve" ssh-ed25519 AAAA... ledge@laptop
```
Use the absolute path that `command -v ledge-server` printed above. sshd runs this line instead of whatever the client asked for, so naming the file outright settles where it is. It does not settle where Bun is, which is the other half of the check.
For the Docker deployment, the forced command reaches into the container instead:
```
restrict,command="docker exec -i ledge ledge-server serve" ssh-ed25519 AAAA... ledge@laptop
```
That key cannot forward a port, run `scp`, or open a shell over ssh. What it limits is what the key is good for if it is ever stolen: no route into the network behind that server, and no file copying.
It does not limit Ledge. Blocks in your notes still run, because running them is what the protocol does, so anyone holding that key can run code on that machine. Restricting narrows what else they could do with it.
If you also want to use ssh directly to that machine from a terminal, keep your usual key there as well. The restricted line is for Ledge alone.
A phone's key arrives already restricted. The line its pairing screen hands you carries this prefix ([Ledge on Your Phone](https://ledge.sh/docs/ledge-on-your-phone)).
## Expose ssh carefully
A Ledge server executes the code in your notes. Anyone who can authenticate to it can run anything you could.
On a VPS, bind sshd to a private interface rather than to the public internet. In `/etc/ssh/sshd_config`:
```
ListenAddress 100.x.y.z
PasswordAuthentication no
```
Use the address your VPN or tailnet gives the machine. A Ledge server on `0.0.0.0` is a box on the public internet whose purpose is running code.
`PasswordAuthentication no` and Ledge's password sign-in are the same setting seen from two ends, and the order to do them in is: use a password to reach the machine, put your key on it, then turn passwords off. A box reachable from the internet should not be answering password attempts from it.
On a Mac, the server needs Remote Login turned on in System Settings, under General then Sharing. Restrict it to specific users while you are there.
## What lives on the server
| On the server | On this Mac |
| ------------------------------------------ | -------------------------------------------------------------------- |
| Notes, images, and the trash | Theme, font sizes, and live preview |
| Workspaces | Window size and position |
| The vault and locked notes | The clipboard |
| Profiles and their secrets | Which connections exist, their pinned keys, and any stored passwords |
| Shells, running blocks, and scrollback | |
| The shell, interpreter, and trash settings | |
Settings (⌘,) shows both files. The appearance half follows you between machines; the behavior half describes the machine it is on, because a VPS's shell is not your laptop's.
Profile values never cross the connection. A note names a profile and the server reads the file at spawn, so the secrets exist only where the commands run ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)).
Unlocking a locked note sends the passphrase to the server, which is the only machine that can use it ([Note Locking](https://ledge.sh/docs/note-locking)). The vault and its idle relock timer stay there.
Each device unlocks for itself. Typing the passphrase on your Mac does not open the locked notes on your phone, and ⌘L on one leaves the other reading. Every window on the same Mac shares one unlock.
## Back up the server
Ledge backs nothing up. `ledge-server backup-paths` prints the paths a backup has to cover, and you point a backup tool at them.
Run it as the account the server runs as, on the machine the server runs on:
```sh
ledge-server backup-paths
```
One absolute path per line: the app home, every workspace folder you attached from elsewhere on the machine, and the profiles directory. Only the server can answer this, because only its registry knows where you attached those folders.
| Flag | What it prints |
| -------------- | ------------------------------------------------------------------------------------------------ |
| none | The paths to back up. |
| `--exclude` | What to skip inside them: the daemon's socket and pidfile, the logs, and the copy of the manual. |
| `--no-secrets` | The same list without the profiles directory. |
| `--json` | Both lists, plus any registered folder that is not on disk. |
A registered folder that is missing right now is left out, with a line on stderr saying so. Naming a path that is not there fails the whole backup run, and dropping it without a word is how a workspace stops being backed up until you notice at a restore.
Everything on this page works the same whether the server is a package on a VPS, a build of your own, or the image. The paths differ, so ask the machine rather than assuming them. On a VPS they are all under the account's home. In the image the app home is `/data` and the profiles are under `/home/ledge`, which is why that deployment mounts two volumes.
Ask the container for the image deployment, from its host:
```sh
docker exec ledge ledge-server backup-paths
```
## Back up with restic
restic reads both lists, encrypts on the server before anything leaves it, and keeps versions. Any S3-compatible bucket works: S3, R2, B2, Wasabi, MinIO. [Tutorial: Back Up Your Notes to S3](https://ledge.sh/docs/tutorial-back-up-your-notes-to-s3) sets it up on an hourly systemd timer, with the repository credentials in a profile so a note can run the same backup by hand.
Three things to know before you rely on any backup of a server:
* Keep the backup's password somewhere other than this server. A restore starts on a machine that has nothing on it, and a password stored only inside the backup is a backup you cannot open.
* The backup holds secrets in plain text. Profile values are plain text and so are unlocked notes, so the tool has to encrypt. restic does. `aws s3 sync` and `rclone sync` do not, unless you configure them to.
* Locked notes and the vault travel together or not at all. `.vault.json` is inside the app home, so the printed list already does this. A hand-written list that takes the notes and leaves the vault restores notes Ledge refuses to open ([Note Locking](https://ledge.sh/docs/note-locking)).
* Credentials do not belong in `settings.jsonc`, which is inside the app home and therefore inside the backup. A profile is outside it ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)).
## What a provider snapshot does not do
Most hosts offer whole-server backups. Run both if you like: a snapshot restores a machine, and the backup above restores your notes.
| | Provider snapshot | The backup above |
| ------------------------------ | --------------------------------- | --------------------- |
| Smallest thing you can restore | The whole server | One note, one version |
| Where the copy lives | The account that holds the server | Any bucket you choose |
| Moves to another host | No | Yes |
A snapshot also lives in the account that pays for the server, so a lost login or a lapsed card takes the notes and the copy together.
## Several devices on one server
A server serves every device that connects to it. Your Mac and your phone can both be on the same server at once, reading the same notes and running commands ([Ledge on Your Phone](https://ledge.sh/docs/ledge-on-your-phone)).
Each device keeps its own tabs and panes. The server files them under the device that arranged them, so a phone does not open into a Mac's three-pane layout.
A second Ledge window counts as another device here. Point two windows at one server and each is listed in the other's connection bar, and a note's terminal has one owner between them, exactly as a Mac and a phone would.
The connection bar shows who else is connected: one other device by name, more than one as a count. Hover it for the full list. Names come from the devices themselves, so a Mac uses its computer name, and a device that gives no name reads as "another device".
Nothing appears there when you are the only one connected, and nothing ever appears while your notes are on this Mac.
A note saved on one device appears on the other without a refresh. Everything else a server owns is shared the same way: the same workspaces, the same trash, the same tags and backlinks, the same vault.
The one thing two devices cannot share is a note's terminal.
## Sharing a server with others
A server is one account, and everyone who connects to it shares that account. Anyone else on your server reads every note in every workspace, writes to any of them, and runs commands as the account the server runs as.
Locked notes stay shut on each device until somebody types the passphrase there. There is one vault and one passphrase, so whoever has it opens every locked note on the server ([Note Locking](https://ledge.sh/docs/note-locking)).
Profiles are on the server, and a note names one by name. A note somebody else writes can run a block with your credentials in its environment, without ever opening the file ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)).
Several people can work this way. They get the same notes, the same trash, and the same tags, and every save appears on every device. What they do not get is separation: no per-person access, no per-workspace permission, and no record of who changed a note.
For notes that belong to different people, give each person a server, or keep a clone each over git ([Tutorial: Share Notes with a Git Clone](https://ledge.sh/docs/tutorial-share-notes-with-a-git-clone)).
## Take a shell from another device
Opening a note's terminal on a second device moves the shell there, output and typing together. A shell has one keyboard: two devices typing into the same one would interleave their keystrokes on a single line.
The device that had it keeps the last of the output on screen behind a notice, which names the device that took the shell and offers a Take This Shell button. Press it and the shell comes back, along with everything it printed while it was away.
The shell is unaffected either way. It runs on the server throughout, so a build keeps building while the two devices take turns watching it.
Blocks are different: a block runs for the device that started it, and only that device sees its output panel or can stop it ([Running Code](https://ledge.sh/docs/running-code)).
## Edit the same note on two devices
An open note you are not editing follows its file. Save that note on one device and the other device's copy updates on screen, with no refresh and no prompt.
A note being edited on both devices is settled when they save. The second save wins the file and the version it displaced goes to the workspace trash, so nothing is overwritten. The device that displaced it shows a notice in the sidebar naming the note, and the other version is in the trash until you empty it.
This is the same arbitration a note gets from any other writer, including an agent in the terminal, a `git checkout`, or a sync service ([Tutorial: Keep Notes Synced](https://ledge.sh/docs/tutorial-keep-notes-synced)).
## When the connection drops
The bar reads "reconnecting…" and Ledge re-dials for about thirty seconds. Requests made in the meantime wait rather than fail.
A machine that has been asleep is asked about at once rather than after the usual twenty seconds, because a lid that opens usually opens onto a connection that ended hours ago.
A drop the network does not announce takes twenty to twenty-five seconds to notice. A connection that closes tells Ledge straight away. A network that simply goes away, such as wifi dropping or a laptop moving between networks, sends nothing at all, so Ledge asks the server every 5 seconds and treats three unanswered asks as a lost wire. The same goes for a server that stops answering while the network is fine. The bar still reads as connected until then, and anything you send in that window is held and sent again once the connection is back.
Anything running keeps running. Shells belong to the server and survive a wire dropping, so a build carries on while you are on a train and its output is waiting when you come back.
An open terminal stops taking what you type once the bar reads "disconnected", and covers itself with a notice saying so. Nothing typed at a machine Ledge cannot reach would get there, and a terminal that does not echo looks exactly like one waiting on a slow command, so the notice is what tells the two apart. The last output stays readable underneath it and can still be selected and copied. While the bar reads "reconnecting…" the terminal takes your typing as usual, and it lands when the connection does.
A terminal that was open the whole time fills in its own gap. Ledge asks that shell for its history the moment the connection is back and redraws it, so what printed while you were away is on screen with everything that came before it. The last 256 KB is what a shell keeps, on a reconnect exactly as on any other reattach.
Two other things can have become of that shell while you were away, and the terminal tells you which. Another device may have taken it, which reads the same as it always does: the notice, and the Take This Shell button. Or it may have ended, and the terminal closes rather than reopening on a fresh prompt with none of your work in it.
A block's output panel is the exception, and only across a restart. The panel lives in the page rather than on the server, so a wire that drops and comes back finds it still there with the run still going.
Once the bar reads "disconnected" that panel reads "Disconnected" too, rather than "Running". Ledge cannot see the machine, so it neither claims the block finished nor claims it is still going. The output that already arrived stays on screen and the word says why no more is appearing. When the connection returns the panel goes back to "Running" if the server still has that run, and finishes if it does not.
The block underneath it will not run again meanwhile. It may still be executing over there, and a second copy of a deploy is worse than a wait.
That panel stops taking what you type at it too. A block that was waiting on a password is the case worth getting right: the answer would go nowhere, and nothing would say it had. The line beside the header that reads "typing here" reads "not connected" instead. The caret stays in the panel rather than jumping back to the note, because the connection may be back in seconds, and the panel takes your typing again the moment it is.
What the block printed while you were away arrives when the connection does. The server holds it for you, in the order the shell said it, and the panel adds it to what was already there. A block that finished while you were away comes back finished, with its exit status, rather than closed out blank.
Two limits on that. The last 256 KB is what is held, so a very long outage over a very chatty build loses the middle and keeps the end. And the twenty seconds or so before Ledge notices the connection has gone are lost too: nothing can be held back until something knows there is a reason to.
You cannot start a run at all once the bar reads "disconnected". Every block's Run and terminal buttons go gray, hovering one names the machine that cannot be reached, and ⌘↩ answers with the same sentence. While the bar still reads "reconnecting…" they stay live, and a block run there really does run: the request waits for the connection like any other and goes as soon as it is back. This is the only thing Ledge refuses outright instead of trying and telling you what came back. A run is the one request with no answer to report: Ledge sends it and then listens for output, so a block sent to a machine that is not there would open a panel reading "Running" that nothing would ever correct.
A Ledge that has relaunched has no panel, and no way to show that run or stop it. So blocks left running on a server are stopped the next time Ledge connects to it, which includes switching to another connection and back. A terminal is not affected, because reattaching finds its shell where you left it.
This reaches only the blocks that device started. A server can be carrying runs for more than one of your devices, and a phone connecting does not stop what your Mac left running.
A save that was in flight when the wire dropped is retried once the connection is back, and applied once, even if the first attempt had already landed.
Notes that changed on the server while you were away are re-read as soon as the connection is back. Another device saving, a checkout in a terminal, an agent writing to a note: none of that reaches you while the wire is down, so Ledge asks every open workspace for its list again and re-reads the notes you have open. A note somebody added appears in the sidebar, and an open note you had not touched pours in the newer text.
A note you were editing across a brief drop keeps exactly what you typed. If that note also changed on the server, the two are settled on your next save the same way any two devices editing at once are (see above): your version is saved, and the one it displaced goes to the workspace trash with a notice naming the note.
After a real outage it goes the other way, and it is worth knowing which way before it happens. Once the bar has read "disconnected", Ledge stops trying to save and holds what you type in the tab, with a red dot on it saying so. When the connection comes back, a note nobody else touched is simply saved. A note that did change on the server takes the server's version, and what you had typed goes to that workspace trash with a notice naming the note.
The reason it flips is that the argument for keeping your version is that you are the one at the keyboard, which is exactly what an outage undoes: a laptop shut in a bag for a day, while the same note is edited from a phone, has the older text and the emptier claim to it. Neither version is thrown away either way, and restoring from the trash puts the copy next to the live note rather than over it, so you can merge the two yourself.
Locked notes relock while you are away, and Ledge catches up the moment the connection is back. The vault belongs to the server and shuts itself after 15 minutes with nothing to do ([Note Locking](https://ledge.sh/docs/note-locking)), and a connection that is down is 15 quiet minutes. So a locked note you had open goes back to its placeholder on the reconnect, exactly as if you had pressed ⌘L, and unlocking again pours it back. Unlocking on another machine reaches you the same way: a note sitting behind its placeholder opens as soon as the connection is back.
Anything you typed into a locked note during the outage goes with it. That text could not have reached the disk either way, because writing a locked note needs the vault open, so copy it somewhere else before you reconnect if you want to keep it.
If the re-dialling runs out, the bar reads "disconnected" and Ledge stops accepting work for a machine it cannot reach. It keeps trying underneath, every thirty seconds, for as long as the window is open, so a laptop that wakes up on a working network reconnects itself with nothing for you to do. Clicking the bar tries immediately instead of waiting for the next attempt.
Ledge also tries the moment your machine wakes and the moment your operating system says the network is back, which are the two times a connection that has been failing for hours suddenly works.
Reconnecting to a server that has restarted meanwhile works too, and it is what usually happens after a long sleep: a server with nobody connected shuts itself down after a minute, and connecting starts a fresh one. Your notes are on its disk and are all there. What does not survive is anything that was only in the old server's memory, so shells and running blocks are gone, and the terminal and any output panels say so rather than showing you a prompt that is not there.
A short absence keeps them. Ledge asks every server it connects to hold its shells for five minutes after the connection ends, which covers a lift, a lid closed for a meeting, or a walk between buildings.
Ledge also stops when the server hangs up on purpose rather than the wire failing, and hovering the bar says why. The reason is a second copy of Ledge on this same device connecting to it: the server keeps the newer connection and tells the older one, which stops instead of the two taking the server off each other in a loop. Another device connecting is not a reason (see above).
## Limits
* No accounts. A server is one account's notes, and anyone who can connect to it has all of them (see above).
* One connection per window. Search, tags, backlinks, and wikilinks all stay within the machine that window is on. Open a second window for a second machine (see above).
* One device at a time in a note's terminal. Everything else on a server is shared by every device connected to it (see above).
* No moving a note between servers from inside the app. Use `rsync` or `git`; the notes are ordinary files ([Tutorial: Keep Notes Synced](https://ledge.sh/docs/tutorial-keep-notes-synced)).
* No offline editing. The server has to be reachable to open a note.
---
# Ledge on Your Phone
Source: https://ledge.sh/docs/ledge-on-your-phone
> Ledge runs on an iPhone or iPad as a window onto a server.
Ledge runs on an iPhone or iPad as a window onto a server. The phone holds no notes: it reaches a server over ssh, the way a Mac does in [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server), and shows you what is there.
Set the server up first. "Install the server" on that page is the whole of it, and a server that already serves your Mac needs nothing more.
## Pair with a server
The first launch opens on "Pair with a server", and the screen has three parts.
The first is a key line. On its first launch the phone makes a key of its own in the Secure Enclave, and that key never leaves the phone: there is no file to copy in or out. What leaves is the public half, as one line for the server's `~/.ssh/authorized_keys`:
```
restrict,command="ledge-server serve" ecdsa-sha2-nistp256 AAAA... ledge-iphone-3f2a91c0
```
Copy line puts it on the phone's pasteboard. Share line hands it to AirDrop, Messages, or any app that can carry it to a machine with a shell on the server, which is where the pasteboard on a phone falls short. Add it to `~/.ssh/authorized_keys` there. The comment at the end names the phone, so the line is easy to find again when you want to revoke it.
The line arrives already restricted, in the way "Restrict the key to Ledge" on [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server) describes: the phone's key can speak Ledge's protocol and nothing else. It names `ledge-server` by its bare name, so the server has to be on the PATH an incoming ssh gets ("Check that ssh can find the server" on the same page). For the Docker deployment, change the command in the line to the `docker exec` form shown there.
The second part is the machine: `user@host`, and a port when sshd is not on 22. A phone reads no `~/.ssh/config`, so write the address out.
The third is Connect. The phone dials the server, shows its host key fingerprint, and asks "Is this the server?" alongside the command that prints the same fingerprint on the server. Trust pins the key, and a server that later presents a different one is refused, the same as on a Mac.
## Sign in with a password instead
Choose "A password" under Sign in with and type the password for that account. The phone keeps it in its own keychain, and no key line has to be installed.
The trade-off is the one described on [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server): a password reaches a fresh machine today, and a key is the better long-term answer. A server with `PasswordAuthentication no` refuses it.
## When the phone cannot reach its server
A server slow to answer shows "Connecting to user\@host…" and, after a few seconds, a Choose a Different Server button.
A server that cannot be reached shows "Ledge could not reach a server." with the reason, then Try again and Choose a server. Try again comes first because the usual cause is the phone having moved networks, not the server having moved. Choose a server opens the Servers list, where you pick another one or add one.
A host key that has changed, or a key or password the server no longer accepts, lands you back on the pairing form with the address filled in. Retrying cannot fix either, so the pin is dropped and you compare the fingerprint again.
Removing the last server returns the phone to the pairing screen. Deleting the app deletes its key with it, so a reinstalled phone is a new device to every server and needs its line installed again.
## More than one server
Inside the app the connection bar works as on a Mac: tap it to add, edit, remove, or switch servers, with the same fingerprint step ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)). The form shows the phone's key line where a Mac's shows a key path, with Share Line beside Copy Line.
A phone and a Mac can be on one server at once. Each keeps its own tabs, and a note's terminal has one owner between them.
## What a phone does
| On a phone | Not on a phone |
| ----------------------------------------------------------------- | --------------------------------- |
| Reading, editing, and creating notes, with live preview | The terminal drawer |
| Quick open, full-text search, tags, backlinks, the outline | Attaching a folder as a workspace |
| Daily notes, templates, wikilinks | Moving a workspace folder |
| Images, added from the photo library | |
| Running a block inline, with the host picker and the confirmation | |
| Editing a note's profile | |
| Unlocking locked notes | |
| The trash | |
| Switching workspaces and servers | |
The pages for those features say how each works on a touch screen: Run on every block, the Code Block button, and the control keys above the keyboard in [Running Code](https://ledge.sh/docs/running-code), the photo library in [Images](https://ledge.sh/docs/images), the mode chips under the search field in [Finding Things](https://ledge.sh/docs/finding-things), and splits in [Panes and Tabs](https://ledge.sh/docs/panes-and-tabs).
Tapping through the tree reuses one tab rather than filling the strip, since a note you tap opens as an italic preview ([Panes and Tabs](https://ledge.sh/docs/panes-and-tabs)). A long press on the tab holds Keep Tab Open, which is what makes it stay, and so does typing in the note. It matters more here than on a Mac: there is no ⌘W, so a strip that filled up would take a long press and a menu item per tab to empty.
A block keeps running on the server while the app is in the background, and what it printed is waiting when you come back. A program that needs a whole terminal belongs in a Mac's drawer on the same server.
Unlocking a locked note asks for the passphrase every time. The phone stores none of it, and Face ID does not stand in for it. The relock timer is the server's, so a phone put away for an hour finds its locked notes closed again ([Note Locking](https://ledge.sh/docs/note-locking)).
The manual a phone shows is the connected server's copy, so it describes the version of Ledge that server runs.
---
# Daily Notes and Templates
Source: https://ledge.sh/docs/daily-notes-and-templates
> A template is an ordinary note marked template: true in its frontmatter, ready to be stamped into new notes.
A template is an ordinary note marked `template: true` in its frontmatter, ready to be stamped into new notes. Daily notes build on that: ⌘J opens today's note, creating it from your daily template if you have one.
## Create a template
Run "New Template" from the command palette (⇧⌘P). You get a pre-marked note whose body is its own cheatsheet: the marker line, the tokens, and what carries over.
To convert a note you already have, run "Make This Note a Template". "Remove Template Marker" reverses it. Exactly one of the two shows for a given note.
The marker is one frontmatter line (see [Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments)):
```
---
template: true
---
```
A template is still a normal note. It lives in your workspace, you edit it like anything else, and its H1 is its name in the picker.
## Create a note from a template
⌥⌘N opens New Note from Template: the command palette filtered to one entry per marked note, current workspace's templates first, other workspaces' labeled with their name. Pick one and a new note opens in the current workspace.
Three things change on the way in:
* The `template:` line is stripped, so the new note is not itself a template.
* The `{{tokens}}` are substituted.
* The H1 is set to the new note's title, so it does not inherit the template's name.
Everything else carries over: frontmatter such as `cwd` and `tags`, the body, code blocks, and links. Substitution reaches inside fences, so a `prompt` block reading `Summarize [[{{yesterday}}]]` becomes a real link when the note is stamped.
## The tokens
Write these anywhere in a template:
| Token | Becomes |
| --------------- | -------------------------------------- |
| `{{date}}` | The creation date, local `YYYY-MM-DD`. |
| `{{time}}` | The creation time, local `HH:MM`. |
| `{{title}}` | The new note's title. |
| `{{yesterday}}` | The previous calendar date. |
| `{{tomorrow}}` | The next calendar date. |
Anything else in doubled braces is left as written, so shell syntax and unrelated `{{placeholders}}` in code blocks survive untouched.
## Daily notes
⌘J opens today's note, titled with today's local date, such as `2026-07-19`. If it exists, it opens; if not, it is created. Press ⌘J as often as you like.
The date is local wall-clock time, so a note started at 11pm belongs to today, not tomorrow.
By default the daily note lives in the selected workspace. To send every day to one place, set `daily.workspace` in Settings (⌘,) and ⌘J goes there from anywhere.
Inside that workspace it sits at the top level. Set `daily.folder` to put every day in a folder instead, like `journal` or `log/2026`, and ⌘J files it there from then on.
Both settings only decide where a day's note is created. An existing one is found by its date wherever it already sits, so changing either leaves the notes you already have where they are.
## The daily template
Mark one note in the daily workspace with `template: daily` and every fresh daily note is stamped from it, tokens included. A template with `[[{{yesterday}}]]` near the top gives every morning a link to the day before.
The daily role is per-workspace. A workspace with no daily template gets a bare dated note. "Edit Daily Template" in the palette opens the right note, or "New Daily Template" if none exists yet.
In the sidebar, templates show a layout glyph in place of the file icon, and the daily template shows a calendar.
From a terminal, `ledge today` opens the same daily note. See [The ledge CLI](https://ledge.sh/docs/the-ledge-cli).
---
# Images
Source: https://ledge.sh/docs/images
> A Markdown image reference alone on its own line renders as the picture.
A Markdown image reference alone on its own line renders as the picture. The fastest way to get one there is to paste it.
## Paste an image
Copy an image anywhere, a screenshot or a picture from the web, and press ⌘V in a note. Ledge saves the image into the workspace and inserts the reference:
```

```
Ledge embeds only when the pasteboard holds an image and no text. A pasteboard holding text pastes as text, converted from formatting where there is any ([Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces)).
The file lands in `.ledge-assets/` inside the workspace folder, named by paste date. One folder holds a workspace's images wherever the note that pasted them sits, so two notes can show the same picture. An attached project folder carries its pasted images with it, and the references stay relative. The folder is dot-prefixed so that Ledge's writes are identifiable inside a real project.
A reference points at the file from the note that holds it, the way Markdown references work everywhere else. A note at the top of a workspace writes `.ledge-assets/pasted-2026-07-19.png`; a note one folder down writes `../.ledge-assets/pasted-2026-07-19.png` for the same file. Ledge writes the right one when you paste, and the note renders in any other Markdown editor too.
## Insert a picture you have not copied
Run **Insert Image…** from the palette to pick a picture instead of pasting one. Ledge saves it and inserts the reference exactly as a paste does.
On a Mac this opens a file dialog. On a phone it opens the photo library, and the button for it sits on the bar above the keyboard: a phone has no ⌘V, so this is the way pictures get into a note there.
A picture chosen this way is saved as a JPEG when it already is one (a photograph off a camera roll stays a tenth of the size it would be as a PNG), and as a PNG otherwise. Location data is not carried over.
## What renders
Two kinds of source draw as images:
* **Web URLs.** `` loads from the network.
* **Workspace files.** `.ledge-assets/` pastes, and any image already in the workspace folder, referenced relative to the note. A note in an attached project can show the project's own `img/logo.png`, and a note in a subfolder reaches it with `../img/logo.png`.
Supported formats are png, jpeg, gif, webp, avif, and svg. Absolute paths stay as text. A reference that climbs out of the workspace folder draws as a broken image, the same as one naming a file that is not there.
An image renders when its reference sits alone on a line and your caret is elsewhere. Click the picture, or move the caret onto its line, and it reverts to editable Markdown; move away and it draws again. This is the same reveal-on-touch behavior as tables and links.
A selection that only passes over an image leaves it drawn, tinted to show it is included. Dragging or shift-arrowing across a picture therefore selects the text on both sides of it without the page moving under you. What decides is where the selection started: begin on the image's own line and the Markdown shows, as it does for the caret.
A reference inline in a sentence stays compact instead: the syntax is concealed and the alt text is styled like a link, so the line does not reflow while you read.
## Deleting notes and images
Deleting a note leaves its images in place. A stray unused image is cheaper than an image another note still references.
Moving a note to another folder leaves them in place too, and rewrites the note's references so they still point at the same files. A locked note has to be unlocked first, because its references are inside the encrypted body.
The files are ordinary files, so you can grep them by name and sync them with the workspace.
An image pasted into a locked note is encrypted on disk from the first byte, and locking a note seals the images it references. See [Note Locking](https://ledge.sh/docs/note-locking).
---
# Note Locking
Source: https://ledge.sh/docs/note-locking
> Locking encrypts a note's body on disk behind a passphrase.
Locking encrypts a note's body on disk behind a passphrase. It protects the note from three readers, in this order:
* **Agents.** Ledge points AI tools at your notes, and a locked body is never available to them, even while you have the note open.
* **Sync services.** A locked note leaves the machine as ciphertext.
* **Anyone at your screen.** Reading a locked note requires an unlock.
## Lock a note
Run "Lock This Note…" from the command palette.
The first lock sets up the vault. You choose a passphrase, typed twice, and the dialog states the contract: there is no recovery. A forgotten passphrase means the note's body is gone.
Locking then asks once, every time, and states one limit. Encrypting a note now does not reach backwards. Copies taken before this moment stay plain text where they were taken: a sync service's version history, a backup, or a git commit. Only a note locked from the start has a clean history. To retract a note that was committed in the open, rewriting the git history with `git filter-repo` is the only thing that does it.
Locking a note also seals the images it references, since a screenshot pasted into a sensitive note is often the most sensitive thing in it.
## Remove a lock
"Remove Lock…" decrypts the note, and its images unless another locked note still uses them, behind one confirmation. After that the next sync or agent scan sees the body.
## Unlock and relock
One passphrase covers every locked note, on the device you type it on.
* Opening a locked note while the vault is shut asks for the passphrase there. A wrong passphrase shakes and lets you retry.
* "Unlock Notes…" in the palette asks for it up front.
* ⌘L runs Lock Notes, which relocks immediately. Use it when you walk away.
* The vault also relocks itself after 15 minutes in which no note changed. Reading a note does not hold it open, and neither does an agent working in your notes while you are away.
(Illustration: a locked note shows only a lock glyph and an Unlock Notes button. Entering the vault passphrase in the Unlock Notes dialog brings the note's body back.)
Unlocking covers every window on that Mac, so opening a second window does not ask again.
Once unlocked, every locked note reads and edits like a normal note, and saves go back to disk encrypted. Locked notes show a lock glyph in the sidebar and in ⌘P, drawn open while the vault is unlocked, so you can see what is readable without opening anything.
"Change Vault Passphrase…" rewraps every locked note under the new passphrase, leaving contents untouched, and reports how many it found.
A locked note is self-contained. Carried to another of your machines, it unlocks with the passphrase alone, with no vault file to bring along. One ordering rule comes with that, under "Recover locked notes from a backup" below.
## Unlocking on more than one device
If you keep notes on a server and reach them from more than one device ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)), each device unlocks for itself.
Unlocking on your Mac does not unlock your phone. Both use the same passphrase, and neither can see the other's locked notes until it is typed there. ⌘L works the same way: it locks the device you run it on and leaves the others reading.
This is the behavior a stolen phone needs. Somebody holding an unlocked, paired device still has to know the passphrase to open a locked note on it.
## Recover locked notes from a backup
Locked notes restore from git, S3, or any backup that carried the files, on any machine, with the passphrase alone. Each locked note and each sealed image carries what it needs to be decrypted, so there is no key file to keep safe alongside them.
One rule makes that true, and it is about order:
**Restore your notes before you lock anything new on the new machine.**
Ledge derives its key from your passphrase and a random salt. Restored notes carry the salt they were locked under. A machine that has never locked anything adopts that salt from the notes themselves, which is what lets them open. A machine that locked something first has already minted a salt of its own, and then the same passphrase produces a different key: your restored notes stay shut, and the passphrase being right is what makes that confusing.
If it happens, nothing is lost. Restore `.vault.json` from the same backup into `~/.ledge`, which puts the original salt back, and the notes open. Backups made with the `ledge backup-paths` recipe include that file already ([Tutorial: Back Up Your Notes to S3](https://ledge.sh/docs/tutorial-back-up-your-notes-to-s3)).
Locked notes from someone else's Ledge are a different matter, and they do not open. The next section says why.
## Sharing locked notes
Locked notes are yours, on your own machines. There is one passphrase and one vault, so reading a locked note takes your key and nothing else.
Sending a locked note to somebody without the passphrase gives them nothing. If they already lock notes of their own, Ledge says the note was locked by a different vault.
Sending the passphrase with it does open the note, on a machine that has never locked anything. The note carries the salt it was locked under, and a machine with no vault adopts it (see above). That salt and your passphrase then become their vault, so everything they lock afterwards opens with your passphrase. Nothing on screen says so.
A workspace shared over git carries locked notes as ciphertext ([Tutorial: Share Notes with a Git Clone](https://ledge.sh/docs/tutorial-share-notes-with-a-git-clone)). That suits a backup and does not suit collaboration.
To share a secret with someone, use a profile ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)). Profiles live outside every notes folder, so they never travel with a workspace in the first place.
## What stays visible
Locking hides the body. It does not hide:
* **The title**, as the filename and the H1, everywhere titles appear: the sidebar, ⌘P, and wikilinks. This is what keeps navigation and linking working without decryption. If the title itself is the secret, give the note a bland one.
* **The frontmatter**, including tags. A tag on a locked note still shows in the tags panel.
* **The file's existence**, its size, and its modification time.
Full-text search, backlinks, and tag scans skip locked bodies. The panels report how many notes they skipped, so a partial answer looks partial ([Finding Things](https://ledge.sh/docs/finding-things)).
## How locking interacts with the rest of Ledge
* **Agents** ([Agents and Ledge](https://ledge.sh/docs/agents-and-ledge)) cannot read locked bodies. Reading a locked note over MCP or the CLI returns a refusal, and listings flag locked notes so agents can plan around them.
* **`prompt` fences** in a locked note have their run buttons disabled, since their job is to send the body to an agent.
* **Other code blocks** in a locked note still run. The commands in a locked ops note are yours to run.
* **Templates** cannot be locked. A template's body exists to be stamped into new notes.
* **The `locked:` frontmatter line** is machine-owned. Deleting it in the editor decrypts nothing; only "Remove Lock…" does.
## Limits
Locking protects notes at rest and from the software Ledge invites in. It does not defend against malware running as you, or against a stolen machine that is already unlocked. Use FileVault for stolen hardware. Locking sits on top of it, not instead of it.
---
# Agents and Ledge
Source: https://ledge.sh/docs/agents-and-ledge
> Ledge is built to be worked by AI agents as well as by you.
Ledge is built to be worked by AI agents as well as by you. An agent CLI such as Claude Code can read, search, create, and edit your notes through Ledge's MCP server. A terminal launched inside a note already knows which note it is in, and a `prompt` code fence turns a paragraph of instructions into a runnable block.
## Connect an agent
Ledge ships an MCP server. `ledge mcp` serves it on stdio, so install the `ledge` command first (see [The ledge CLI](https://ledge.sh/docs/the-ledge-cli)). Any MCP-speaking agent can use it. For Claude Code it is one line, in your own terminal:
```sh
claude mcp add ledge -- ledge mcp
```
The server exposes eleven tools:
| Read | Write |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `list_workspaces`, `list_notes`, `read_note`, `search_notes`, `backlinks`, `tags`, `settings` | `create_note`, `daily_note`, `append_note`, `edit_note` |
Notes are addressed by title, which survives renames, so an agent's references do not go stale. Every tool goes through the same store and the same path guards as the app.
Two boundaries hold in every case: there is no delete tool, and locked notes refuse their bodies to every agent surface (see [Note Locking](https://ledge.sh/docs/note-locking)).
## Agents and folders
Two notes in different folders may share a title, so `list_notes` tells them apart: every row says which folder its note is in, and a note at the top level says nothing. Read a note by that title and the answer names its folder too.
Listing, searching, and the tag tools take a `folder` to narrow to one, and it covers the folders inside it as well. The tools that address a note by title take one too, which is how an agent says which of two notes sharing a title it means. `create_note` takes one to place a new note, creating the folder if it is new, and `daily_note` takes one for the day it creates today's note.
Without a folder a new note lands at the top level of the workspace, which is where your own New Note puts one. There is no tool for moving a note afterwards, and none for renaming or deleting a folder: filing is yours, in the sidebar ([Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces)).
## Agents know which note they are in
Every shell a note spawns carries two environment variables: `LEDGE_NOTE`, the note's file, and `LEDGE_WORKSPACE`, its workspace folder. An agent launched in a note's terminal drawer picks these up through the MCP server:
* `read_note` with no arguments reads the note the terminal belongs to.
* `append_note` and `edit_note` default to that note.
* `create_note` lands in its workspace, at the top level unless it names a folder.
So "summarize this note" or "add a TODO section here" needs no explanation of what "this" means. Open the note you are working in, press ⌃\` for its terminal, start your agent, and talk about "this note" and "this workspace" in plain words.
## Prompt fences
A fenced block whose language is `prompt` is an agent run. Write instructions in it and press ⌘↩. The block's text is piped to the agent CLI in one-shot mode, and the output streams into the panel below like any other run ([Running Code](https://ledge.sh/docs/running-code)).
A prompt fence in a release note, run with ⌘↩:
```prompt
Summarize this note in one sentence for the deploy channel.
```
Output:
```txt
2.4.2 ships the retry-queue fix and invoice export; staging
has soaked clean for two days and prod flips tomorrow 09:00.
```
The answer streams into the panel beneath it. Instructions can also change things: "append a Next steps section to this note", or "create a note titled Retro from what we discussed above".
The block runs from the note's own shell, so the agent inherits the note's `cwd`, `env`, and the environment variables above.
Two things to expect. There is a pause before the answer appears, because one-shot mode thinks first and prints once. And since nobody is present to answer follow-up questions, the agent is instructed to act and report rather than ask.
By default the fence runs Claude Code (`claude -p`) with Ledge's own tools pre-authorized, because a non-interactive run has no one to click "allow". The command is an interpreter entry in Settings (⌘,) under `blocks.interpreters`, key `prompt`. Point it at any CLI that reads its prompt on stdin to switch agents.
A daily template carrying a prompt fence such as `Summarize [[{{yesterday}}]]` gives every day's note a one-keystroke briefing (see [Daily Notes and Templates](https://ledge.sh/docs/daily-notes-and-templates)).
## What agents cannot see
Agents see the titles, bodies, tags, and links of ordinary notes. They can read the manual inside the app too, so "check the Ledge docs" is a fair instruction.
They never see the body of a locked note. Reads refuse with an explanation, searches skip locked notes and report how many they skipped, and listings flag them so an agent can plan around it.
Settings are readable but not writable. The `settings` tool shows an agent your `settings.jsonc` with its comments, so it can answer "which python is that block using" from your actual configuration and name the line to change. Making the change is yours, in the app (⌘,), and it applies at the next launch.
Deletion is yours alone, in the app, where the trash and Undo live.
## The manual for LLMs
An agent without Ledge's MCP server can still read this manual. The site serves it in forms made for LLMs, built from the same pages you are reading, so they change when the docs do:
* [/llms.txt](https://ledge.sh/llms.txt) lists every page with a one-line description.
* [/llms-full.txt](https://ledge.sh/llms-full.txt) is the whole manual in one Markdown file.
* Any page with `.md` added to its address is that page as Markdown, for example [/docs/agents-and-ledge.md](https://ledge.sh/docs/agents-and-ledge.md).
To give an agent the full manual in one step:
```sh
curl -s https://ledge.sh/llms-full.txt
```
---
# The ledge CLI
Source: https://ledge.sh/docs/the-ledge-cli
> The ledge command lists, reads, searches, creates, and appends to notes from any terminal.
The `ledge` command lists, reads, searches, creates, and appends to notes from any terminal. 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, or `ledge install` if you already have the binary somewhere.
It writes a small shim onto your PATH (Homebrew's bin, `/usr/local/bin`, or `~/.local/bin`, whichever works) pointing at this copy of Ledge. If you move the app, run it again.
The palette offers this only while your notes are on this Mac. `ledge` ships with the app and a server carries no copy of it, so the command is absent whenever Ledge is pointed at a remote server ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)).
## The verbs
`ledge help` prints the full usage.
| Verb | What it does |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `ledge ls` | Lists notes. |
| `ledge search ` | Prints `path:line: match` rows like grep, and exits nonzero on no hits. |
| `ledge cat ` | Prints a note's Markdown. |
| `ledge tags` | Lists the workspace's tags with counts. `ledge tags ` lists the notes bearing one. |
| `ledge workspaces` | Lists the workspace roots. |
| `ledge new ` | 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 ` | Appends to a note, or to one heading's section with `--heading`. |
| `ledge today` | Opens today's daily note in the app. |
| `ledge ` | Opens the app at that note. `ledge` alone just opens the app. |
In a terminal, once the shim is on your PATH:
```sh
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](https://ledge.sh/docs/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:
```sh
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 ` 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](https://ledge.sh/docs/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](https://ledge.sh/docs/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 ` targets a specific workspace, `-f ` targets a folder, and `--all` widens `ls` and `search` to every workspace.
## 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](https://ledge.sh/docs/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.
---
# Tutorial: Run a Project from a Note
Source: https://ledge.sh/docs/tutorial-run-a-project
> Write one note that builds, tests, and runs a project, and keep it with the rest of your notes.
Write one note that builds, tests, and runs a project, and keep it with the rest of your notes.
This uses [Running Code](https://ledge.sh/docs/running-code), [Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments), and [Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces).
## 1. Write a playbook note
Create a note with ⌘N and give it the commands you actually run, as fenced blocks:
````
# Playbook
## Setup
```sh
npm install
```
## Test
```sh
npx vitest run
```
````
The project's routine is now documentation and buttons at once. ⌘↩ runs a block inline, with the output next to the prose. ⇧⌘↩ sends it to the terminal drawer when you want to keep interacting.
The inline shell persists, so an exported variable or an activated virtualenv from the Setup block is still there when the Test block runs.
## 2. Point it at the project
So far those blocks run from your home folder, the default for notes in a managed workspace. One frontmatter line, added with ⌥⌘,, starts them in the project instead:
```
---
cwd: ~/Projects/my-app
---
```
`cwd` sets the working directory for every shell the note spawns, the inline one and the terminal drawer both. The note itself stays where it is, in your notes workspace or a synced folder. Only the shells move.
## 3. Give it an environment
If the project keeps configuration in a dotenv file, add a second line:
```
---
cwd: ~/Projects/my-app
envFile: .env
---
```
`envFile` resolves against the note's `cwd`, so this picks up the project's own `.env`. For real secrets, use `profile: myproject` instead. The values live in a file outside the notes, so your notes folder and anything syncing it never carry credentials. See [Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets).
## When the project should be the workspace
A project that deserves many notes rather than one can become a workspace itself. Run "Attach Folder as Workspace…" from the command palette (⇧⌘P) and pick the project's folder. Every `.md` file already in it, such as the README and the docs folder, becomes a note, and every note's shells start in the project folder with no `cwd` needed.
Vendor and build directories such as `node_modules` are ignored. If the listing still shows Markdown you do not want as notes, add a `.ledgeignore` file in the folder root with one gitignore-style pattern per line. See [Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces).
## Where to go next
Each of these is one line of frontmatter or one new block away:
* A deploy note with `host: deploy@prod` runs its blocks on the server instead of your machine ([Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts)).
* A `prompt` fence such as "Read the test output above and suggest a fix" puts an agent in the loop ([Agents and Ledge](https://ledge.sh/docs/agents-and-ledge)).
* Wikilinks tie the playbook to design and incident notes, and backlinks (⌥⌘L) tie them back ([Finding Things](https://ledge.sh/docs/finding-things)).
The habit worth building: when you catch yourself typing the same commands twice, put them in the note where their context lives and run them from there.
---
# Tutorial: A Daily Workflow
Source: https://ledge.sh/docs/tutorial-a-daily-workflow
> Set up a daily rhythm: one keystroke opens today, today links to yesterday, and nothing you jot down goes missing.
Set up a daily rhythm: one keystroke opens today, today links to yesterday, and nothing you jot down goes missing.
This builds on [Daily Notes and Templates](https://ledge.sh/docs/daily-notes-and-templates).
## 1. Write a daily template
Run "New Daily Template" from the command palette, or "Edit Daily Template" if you already have one. Give it the shape you want every morning to start with:
```
---
template: daily
---
# Daily Template
← [[{{yesterday}}]]
## Plan
## Log
## Done
```
That is the whole setup. From now on ⌘J opens today's note, stamped from this template: the H1 becomes today's date, `{{yesterday}}` becomes a link to the day before, and the headings are ready.
The arrow link lets you walk backward through your days. The backlinks panel (⌥⌘L) on any day shows the day after, so the chain works in both directions.
## 2. Capture through the day
Press ⌘J any time, from any workspace, and you land in today. Jot into Log as things happen.
When a thought belongs to a project rather than the day, put a `#tag` or a `[[wikilink]]` on its line. The daily note stays a chronological stream, and the tags panel (⌥⌘T) or the linked note's backlinks reassemble the thread by topic later.
You can also capture without switching to the app, using [The ledge CLI](https://ledge.sh/docs/the-ledge-cli):
```
ledge append 2026-07-19 -m "- deploy went out at 14:10" --heading Log
```
That works from any terminal, addressing today's note by its date title. Inside any note's terminal drawer, a bare `ledge append -m "…"` appends to that note.
## 3. Close the day
At the end of the day, move what mattered from Log to Done and carry the rest into tomorrow's Plan, one ⌘J away after midnight.
To have an agent do the remembering, add a `prompt` fence to your template:
```
Read the note [[{{yesterday}}]] and reply with a three-bullet summary: what got done, what is still open, what looked risky.
```
Give that fence the `prompt` language and each morning's briefing is one ⌘↩ away ([Agents and Ledge](https://ledge.sh/docs/agents-and-ledge)).
## Finding it again
Weeks later, the date titles carry the when, full-text search (⌥⌘P) carries the what, and the yesterday-links carry the story in between.
---
# Tutorial: Pair with an Agent
Source: https://ledge.sh/docs/tutorial-pair-with-an-agent
> Get an AI agent working inside your notes, three ways: a conversation in a note's terminal, one-shot prompt fences, and capture from anywhere.
Get an AI agent working inside your notes, three ways: a conversation in a note's terminal, one-shot prompt fences, and capture from anywhere.
This builds on [Agents and Ledge](https://ledge.sh/docs/agents-and-ledge). The examples use Claude Code, but any MCP-speaking CLI works.
## 1. Connect the agent
Install the shell command with "Install Shell Command (ledge)" in the palette, then connect the agent to Ledge's MCP server from a terminal:
```sh
claude mcp add ledge -- ledge mcp
```
## 2. Talk to an agent in a note
Open a note you are working on, press ⌃`for its terminal drawer, and start`claude\`.
The terminal belongs to the note, so the agent already knows what "here" means. Ask it to "read this note and tell me what's missing", "add a section comparing the two options above", or "find my other notes that mention this design". No paths, and no copying text into a chat window. Edits land in the file you are looking at, live.
Use this for working sessions: you write, the agent reads and reacts, and everything it adds is in your note when the session ends rather than in a chat log.
## 3. Turn repeated asks into prompt fences
For an instruction you give repeatedly, write it into the note as a `prompt` fence and it becomes a button. A meeting note might end with:
```
Extract every action item from this note into a checklist under a new "Actions" heading, with owners in bold.
```
Give that fence the `prompt` language and after every meeting it is one ⌘↩.
The fence runs one-shot: the agent acts and reports rather than asking questions, so write the instruction with the decisions already made.
Fences work in templates too ([Daily Notes and Templates](https://ledge.sh/docs/daily-notes-and-templates)). A template carrying a fence gives every stamped note a built-in agent action, like the morning briefing in [Tutorial: A Daily Workflow](https://ledge.sh/docs/tutorial-a-daily-workflow).
## 4. Capture from anywhere
An agent that can run shell commands can work your notes with the CLI alone, with no MCP setup ([The ledge CLI](https://ledge.sh/docs/the-ledge-cli)).
Tell a Claude Code session in any project to "append what we just decided to my Decisions note" and `ledge append` gets it there. `ledge search` and `ledge cat` let it check your notes before answering. Your notes become memory that outlives any one session.
## The boundaries
Agents can read and write notes but never delete them. The trash and Undo stay yours, in the app.
A locked note's body is invisible to every agent surface ([Note Locking](https://ledge.sh/docs/note-locking)). Lock the notes you want kept out of the loop.
---
# Tutorial: Keep Notes Synced
Source: https://ledge.sh/docs/tutorial-keep-notes-synced
> Ledge notes are plain files in ordinary folders, so syncing them is just syncing a folder: iCloud Drive, Dropbox, git, Syncthing, or whatever you already use.
Ledge notes are plain files in ordinary folders, so syncing them is just syncing a folder: iCloud Drive, Dropbox, git, Syncthing, or whatever you already use.
This page covers the two common setups, and how to move notes that are not in the right place yet.
## What a workspace folder holds
Everything a workspace is lives in its folder:
* The notes, as `.md` files.
* Pasted images, in `.ledge-assets/`.
* Deleted notes, in `.ledge-trash/`.
* A `.gitignore`, in a workspace Ledge created.
Sync the folder and you have synced the workspace. There is no database on the side.
Two things are not in the folder. Profiles live outside every notes folder, so syncing notes never ships credentials ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)). And locked notes sync as ciphertext: a locked note is self-contained, so on another machine it unlocks with the passphrase alone ([Note Locking](https://ledge.sh/docs/note-locking)).
Sync workspace folders, not `~/.ledge` itself. The app home holds machine-local state alongside the managed workspace folders.
## Setup one: a synced drive
1. Create a folder inside iCloud Drive, Dropbox, or any synced location.
2. Run "Attach Folder as Workspace…" and pick it.
3. On a second Mac, attach the same folder there.
Notes you write are files in the synced folder, and the service carries them.
Ledge watches the folder and follows external changes live, so a note edited elsewhere updates on screen when the sync lands, even if you have it open.
If a synced change arrives while you are editing the same note, your version wins the file and the other version is kept in the workspace trash. Ledge names that note in a notice in the sidebar when it happens, so a conflict costs a visit to the trash rather than a lost edit. When the service makes its own conflict copy, that copy appears as another note, which you can diff and merge by hand.
## Setup two: git
A workspace folder can be a git repository, and an attached project workspace usually already is. For a notes-only repo: create a folder, run `git init` in it, and attach it.
Deleted notes stay out of git on their own. Ledge puts a `.gitignore` inside `.ledge-trash/` that covers the whole folder, so `git add -A` never reaches it. That happens the same way in a workspace Ledge created and in a repository you already had, and your own `.gitignore` is never edited.
The trash empties thirty days after a delete. Git keeps what it was given, so a trashed note that reached a commit is still in the log once Ledge has purged it.
An ignore rule does not untrack what a repository already committed. If your workspace committed its trash before, untrack it once:
```
git rm -r --cached .ledge-trash
```
That removes the trash from future commits and leaves the files on disk. The earlier commits still hold what they held; rewriting them is a `git filter-repo` job and is only worth it for something that should never have left the machine.
Images are committed. `.ledge-assets/` holds the pictures the notes reference, so it is tracked like any other content.
A workspace you create in Ledge arrives with a `.gitignore` for the rest: macOS's `.DS_Store`, and the temp file a save leaves behind if the app is killed mid-write. A folder you made yourself has no such file, so add those two lines if you want them:
```
.*.md.tmp-*
.DS_Store
```
Then let the workspace sync itself, with a note in it:
````
# Sync
```sh
git add -A && git commit -m "notes $(date +%F)" || true
git pull --rebase && git push
```
````
A note in an attached workspace runs its shells in the folder itself, so that block commits and pushes the workspace it lives in, with one ⌘↩ ([Running Code](https://ledge.sh/docs/running-code)). Run it when it matters, or from cron via the CLI to automate it.
You get history for every note, diffs, branches, and hosting anywhere. Plain Markdown makes the diffs readable.
A repository is also how a workspace reaches other people, with a clone each ([Tutorial: Share Notes with a Git Clone](https://ledge.sh/docs/tutorial-share-notes-with-a-git-clone)).
## Move notes you already have
Both setups start with a folder in the right place. Notes in a managed workspace live inside `~/.ledge`, which a sync service will not carry, so the workspace has to move out first.
1. Run "Move Workspace Folder…" from the command palette, or from the workspace's row menu in the sidebar.
2. Pick the destination's parent folder, such as your iCloud Drive folder.
3. Reopen the tabs you were working in. They close during the move.
4. Attach the same folder on your other Mac.
The whole folder relocates: notes, images, and trash together, with references intact. The workspace continues at its new home as an ordinary attached folder.
One limit: the move is a rename, so it cannot cross to a different volume. If you pick a destination on another disk, Ledge tells you rather than copying. Move the folder in Finder yourself, then run "Attach Folder as Workspace…" to pick it up again.
## What syncing does not carry
Syncing workspace folders syncs all of your notes. It does not touch Ledge's own state in `~/.ledge`: your settings, the list of which folders are workspaces, and your window layout. Those are machine-local, since a list of folder paths means little on another Mac.
So setting up a new Mac is a short manual step: install Ledge, attach your synced folders, and redo any settings you care about. If your `settings.jsonc` is heavily customized, keep a copy alongside your notes.
There is nothing else to migrate. Notes are files, so there is no export and no import. Locked notes carry what they need to be decrypted, so on the new machine the passphrase alone opens them, with no vault file to move ([Note Locking](https://ledge.sh/docs/note-locking)).
## Which setup to pick
A synced drive is continuous and needs no ceremony. Git is deliberate and gives you history.
They combine: a synced drive for the always-on workspaces, a git repo for the ones that deserve a log. Either way, files and mtimes are the whole interface, which is why any tool that syncs files can sync your notes.
---
# Tutorial: Set Up a Ledge Server
Source: https://ledge.sh/docs/tutorial-set-up-a-ledge-server
> Turn a fresh Linux VPS into a Ledge server: an account for Ledge, the server package, a key that can do nothing but Ledge, and an sshd that ignores everyone else.
Turn a fresh Linux VPS into a Ledge server: an account for Ledge, the server package, a key that can do nothing but Ledge, and an sshd that ignores everyone else.
This builds on [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server), which is the reference for every step here. The commands assume Debian or Ubuntu. Any Linux with glibc 2.29 or newer works, so substitute your package manager on anything else.
Two accounts appear throughout. `you@vps` is the account your provider gave you, which can `sudo`. `ledge@vps` is the account you create in step 1, which cannot.
## 1. Create an account for Ledge
On the VPS, as your own account:
```sh
sudo adduser --disabled-password --gecos "" ledge
```
The account has no password and no `sudo`. Everything Ledge does on this machine runs as this account: the server, the shells, and every block in every note. A key for it that is ever stolen cannot become root.
If your notes need `sudo`, that is a decision for later, made with `visudo` and as narrow as you can make it.
`adduser` gives the account bash as its login shell, which is one of the two shells Ledge runs blocks in.
## 2. Make a key on your Mac
In a terminal on your Mac:
```sh
ssh-keygen -t ed25519 -f ~/.ssh/ledge -C ledge@laptop
cat ~/.ssh/ledge.pub
```
Leave the key's passphrase empty, or use one your ssh agent already holds. Ledge's ssh runs with no terminal attached, so a passphrase it would have to type at a prompt never gets typed. This is about the key file only: signing in with the account's password is a choice on the form, and [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server) covers it. This tutorial uses a key so that step 8 can turn passwords off.
Copy the printed line, then put it on the VPS as the new account's only key. As your own account there, with the line pasted in place of the placeholder:
```sh
sudo install -d -m 700 -o ledge -g ledge /home/ledge/.ssh
echo 'ssh-ed25519 AAAA... ledge@laptop' | sudo tee /home/ledge/.ssh/authorized_keys
sudo chown ledge:ledge /home/ledge/.ssh/authorized_keys
sudo chmod 600 /home/ledge/.ssh/authorized_keys
```
The line goes in unrestricted for now. Step 7 restricts it, once you know the server works.
## 3. Install the server
Still on the VPS, as your own account:
```sh
curl -fsSL https://bun.sh/install | sudo BUN_INSTALL=/usr/local bash
sudo BUN_INSTALL=/usr/local bun add -g ledge-server
```
Both commands carry `BUN_INSTALL=/usr/local`. Bun puts global commands beside itself, and `/usr/local/bin` is on the short PATH an incoming ssh gets. Without the variable, the server lands in a home directory that ssh never searches.
Nothing else needs installing and no service needs starting. Ledge starts the server over ssh when it connects, and the server exits a minute after the last device leaves, unless a block is still running.
## 4. Check that ssh can find it
From your Mac, as the new account, with the new key:
```sh
ssh -i ~/.ssh/ledge ledge@vps 'command -v ledge-server; command -v bun'
```
Two paths printed means the machine is ready. Keep the first one; step 7 needs it.
Nothing printed means Bun was already installed for one user before you started, and its commands are in a directory ssh does not search. [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server) shows the two symlinks that fix it.
## 5. Add the server in Ledge
Run "Notes On…" from the command palette, choose Add, and fill in the form:
| Field | Value |
| --------------- | ------------------------------------------- |
| Name | Whatever you want the connection bar to say |
| SSH destination | `ledge@vps` |
| Port | Blank |
| Sign in with | A key |
| Key | `~/.ssh/ledge` |
Ledge fetches the machine's host key and shows its fingerprint. Get the same fingerprint from the machine itself, in your terminal on the VPS:
```sh
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
```
Choose "It Matches, Add" when the two agree. Ledge pins the key and refuses any future connection from that address that presents a different one.
## 6. Try it
The connection bar now names the server. Press ⌘N, and the note you create is a file on the VPS. Give it one block:
````
```sh
hostname; whoami
```
````
⌘↩ prints the VPS's hostname and `ledge`. The block ran on the server, as the account you made, and the note never left it.
## 7. Restrict the key to Ledge
Edit `/home/ledge/.ssh/authorized_keys` on the VPS and put a prefix in front of the key, using the path step 4 printed:
```
restrict,command="/usr/local/bin/ledge-server serve" ssh-ed25519 AAAA... ledge@laptop
```
That key can now speak Ledge's protocol and nothing else: no shell, no port forwarding, no file copying. sshd runs the named command whatever the client asks for, so the terminal check in step 4 stops working for this key. That is expected. Your own account is the one for terminals.
The connection you already have keeps working. Ledge's next connection, at the next launch or after a drop, uses the restricted line.
## 8. Turn off passwords in sshd
A Ledge server runs whatever its notes say, so sshd should answer keys and nothing else. Create `/etc/ssh/sshd_config.d/10-ledge.conf`:
```
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
```
The `10-` matters. sshd keeps the first value it reads for a setting, and it reads this directory in name order. Cloud images ship a `50-cloud-init.conf` that turns passwords on, and a file named after it would lose.
Your own account has to sign in with a key from now on. The provider usually installed one when it created the VPS, and this line from your Mac says whether it did:
```sh
ssh -o PasswordAuthentication=no you@vps true
```
If it asks for a password, put a key on that account first, with `ssh-copy-id`.
Then check the configuration and reload, keeping your current terminal open until a second one has logged in:
```sh
sudo sshd -t && sudo systemctl reload ssh
```
## 9. Ban repeated guesses with fail2ban
Keys-only sshd refuses every guess, but a box on the public internet still receives thousands of them a day, and each one costs a log line and a connection slot. fail2ban blocks an address after a few failures.
```sh
sudo apt-get install -y fail2ban
```
Create `/etc/fail2ban/jail.local`:
```ini
[sshd]
enabled = true
backend = systemd
maxretry = 5
bantime = 1h
```
`backend = systemd` reads sshd's log from the journal. Debian 12, Ubuntu 24.04, and anything newer ship without a text `auth.log`, and fail2ban without this line fails to start on them.
```sh
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
```
The second command prints the jail's counts. Ledge never trips it: it connects with a key sshd accepts, and reconnects the same way.
## 10. Close every other port
Only sshd needs to be reachable. Allow it, then turn the firewall on:
```sh
sudo apt-get install -y ufw
sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status
```
Ubuntu has `ufw` already, and the install line does nothing there.
If the VPS is on a tailnet or VPN, allow ssh from that interface alone and drop the public rule:
```sh
sudo ufw allow in on tailscale0 to any port 22
sudo ufw delete allow 22/tcp
```
Then use the tailnet address as the SSH destination in Ledge. A server nobody else can reach has nothing for fail2ban to do, and the previous step does no harm.
## 11. Keep it patched
Security updates for the operating system should install themselves:
```sh
sudo apt-get install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
```
Answer Yes. Ubuntu ships with this on, and the two commands confirm it.
The server itself is a package, and updating it is the install line again:
```sh
sudo BUN_INSTALL=/usr/local bun add -g ledge-server@latest
```
A connection between an app and a server that cannot understand each other is refused with a sentence naming which end to update, so a version that falls behind is reported rather than guessed at.
## Where to go next
* **Back it up.** The notes now live on one disk that belongs to one provider. [Tutorial: Back Up Your Notes to S3](https://ledge.sh/docs/tutorial-back-up-your-notes-to-s3) puts an encrypted copy in a bucket every hour.
* **Add your phone.** Its pairing screen hands you a line for this same `authorized_keys`, already restricted ([Ledge on Your Phone](https://ledge.sh/docs/ledge-on-your-phone)).
* **Install what your notes run.** `git`, a language, a cloud CLI: whatever a block on this machine needs, installed as your own account with `apt-get`.
* **Reach other machines from it.** A note on the VPS can carry `host: prod`, and the VPS makes that ssh connection with a key in `/home/ledge/.ssh` ([Run Code on Remote Hosts](https://ledge.sh/docs/run-code-on-remote-hosts)).
---
# Tutorial: Back Up Your Notes to S3
Source: https://ledge.sh/docs/tutorial-back-up-your-notes-to-s3
> Put an encrypted copy of your notes, and everything Ledge keeps beside them on your server, into an S3-compatible bucket every hour.
Put an encrypted copy of your notes, and everything Ledge keeps beside them on your server, into an S3-compatible bucket every hour.
This builds on [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server), where `ledge-server backup-paths` is described, and on [Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets). It assumes a server set up as in [Tutorial: Set Up a Ledge Server](https://ledge.sh/docs/tutorial-set-up-a-ledge-server): the package on a Debian or Ubuntu VPS, running as an account named `ledge`, with your own `sudo` account beside it.
The backup tool is restic. It reads the two lists `backup-paths` prints, encrypts on the server before anything leaves it, and keeps versions, so one note from last Tuesday is something you can ask for.
## 1. Make a bucket and a key for it
In your provider's console, create a bucket for the backup and an access key pair that can read and write that bucket and nothing else. Any S3-compatible service works.
Write down four things: the bucket's endpoint, its name, the access key ID, and the secret. The endpoint becomes a restic repository address:
| Service | `RESTIC_REPOSITORY` |
| ------------------------------------ | ---------------------------------------------------- |
| Amazon S3 | `s3:s3.amazonaws.com/BUCKET` |
| Cloudflare R2 | `s3:https://ACCOUNT.r2.cloudflarestorage.com/BUCKET` |
| Backblaze B2 | `s3:https://s3.REGION.backblazeb2.com/BUCKET` |
| Wasabi | `s3:https://s3.REGION.wasabisys.com/BUCKET` |
| MinIO or another self-hosted service | `s3:https://HOST:9000/BUCKET` |
## 2. Install restic on the server
On the VPS, as your own account:
```sh
sudo apt-get install -y restic
```
## 3. Put the credentials in a profile
In Ledge, on the server, press ⌘N and make a note called Backups with this frontmatter:
```
---
profile: backup
---
```
Run "Edit Note Profile…" from the command palette and add four rows:
| Key | Value |
| ----------------------- | ----------------------------------- |
| `RESTIC_REPOSITORY` | The address from step 1 |
| `RESTIC_PASSWORD` | A long random string with no spaces |
| `AWS_ACCESS_KEY_ID` | The access key ID |
| `AWS_SECRET_ACCESS_KEY` | The secret |
The password encrypts the backup. Make one with `openssl rand -base64 32`, and keep a copy somewhere that is not this server: a restore starts on a machine with nothing on it, and a password stored only inside the backup is a backup you cannot open.
Saving writes `/home/ledge/.config/ledge/profiles/backup.env` on the server, readable by that account alone. Every block in this note now runs with those four variables set, and the timer in step 5 reads the same file.
## 4. Create the repository and take the first backup
Add three blocks to the Backups note:
````
```sh
restic init
```
```sh
restic backup --files-from <(ledge-server backup-paths) --exclude-file <(ledge-server backup-paths --exclude)
```
```sh
restic snapshots
```
````
Run the first once. It creates the repository in the bucket and prints its ID.
Run the second. `backup-paths` lists the app home, every workspace folder attached from elsewhere on the machine, and the profiles directory, and the `--exclude` list drops the daemon's socket and pidfile, the logs, and the copy of the manual. restic reads both, uploads, and prints how much went.
Run the third. One snapshot, with a time and a hostname. The note is now a button for a backup of the machine it lives on, run before an upgrade or whenever you want to know the last one worked.
## 5. Run it every hour
On the VPS, as your own account, create `/etc/systemd/system/ledge-backup.service`:
```ini
[Unit]
Description=Ledge backup
[Service]
Type=oneshot
User=ledge
EnvironmentFile=/home/ledge/.config/ledge/profiles/backup.env
ExecStart=/bin/bash -c 'restic backup --files-from <(ledge-server backup-paths) --exclude-file <(ledge-server backup-paths --exclude)'
ExecStart=restic forget --keep-hourly 24 --keep-daily 30 --keep-weekly 12 --keep-monthly 24 --prune
```
`User=ledge` runs it as the server's account, which is the account whose registry `backup-paths` reads. `ExecStart` names `/bin/bash` because systemd runs no shell of its own and the two `<(...)` substitutions need one. The second `ExecStart` thins old snapshots to a day of hourlies, a month of dailies, a quarter of weeklies, and two years of monthlies.
Then `/etc/systemd/system/ledge-backup.timer`:
```ini
[Unit]
Description=Ledge backup, hourly
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target
```
`Persistent=true` runs a backup that was missed while the machine was off. Start it:
```sh
sudo systemctl enable --now ledge-backup.timer
sudo systemctl start ledge-backup.service
sudo journalctl -u ledge-backup --no-pager | tail
```
The second line runs one backup now instead of waiting for the hour, and the third shows what it printed. `systemctl list-timers ledge-backup.timer` says when the next one is due.
## 6. Get a note back
Add one more block to the Backups note:
````
```sh
restic restore latest --target /tmp/restored --include '*/shipping-notes.md'
```
````
It puts that one file, from the newest snapshot, under `/tmp/restored` with its original path beneath. `restic snapshots` lists older ones, and any snapshot's ID goes where `latest` is.
Do this once now, with a note you have, before you need it.
## 7. Restore everything onto a new server
On a fresh machine set up through step 4 of [Tutorial: Set Up a Ledge Server](https://ledge.sh/docs/tutorial-set-up-a-ledge-server), with restic installed and the four variables in `/home/ledge/.config/ledge/profiles/backup.env` again by hand:
```sh
restic restore latest --target /
```
The paths inside the backup are absolute, so restoring to `/` puts the app home, the attached folders, and the profiles back where they were. Then connect from Ledge. Your workspaces, images, trash, profiles, and vault are all there, and locked notes open with the passphrase they had ([Note Locking](https://ledge.sh/docs/note-locking)).
## Where to go next
* **Check the repository now and then.** A `restic check` block in the Backups note reads the bucket and reports anything missing or corrupt.
* **Keep the provider's snapshots too.** A snapshot restores the machine, and this backup restores your notes to any machine. [Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server) compares the two.
* **Back up a laptop the same way.** `ledge-server backup-paths` is not on a Mac that runs the app, but a Mac's notes are folders ([Tutorial: Keep Notes Synced](https://ledge.sh/docs/tutorial-keep-notes-synced)).
---
# Tutorial: Share Notes with a Git Clone
Source: https://ledge.sh/docs/tutorial-share-notes-with-a-git-clone
> Put a workspace in a git repository and keep a clone each, so you and the people you work with read and write the same notes.
Put a workspace in a git repository and keep a clone each, so you and the people you work with read and write the same notes.
This uses [Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces), [Running Code](https://ledge.sh/docs/running-code), and the git setup in [Tutorial: Keep Notes Synced](https://ledge.sh/docs/tutorial-keep-notes-synced).
You publish the workspace in steps 1 to 4. Everyone else clones it in step 5, and from step 6 on every side does the same things.
## 1. Put the workspace where git can reach it
An attached workspace is already a folder you chose, and a project workspace is usually a repository already. Either one is ready, so skip to step 2.
A managed workspace lives inside `~/.ledge`, the app's own home. Run "Move Workspace Folder…" from the command palette or the workspace's row menu, and pick a parent folder such as `~/Projects`.
Reopen the tabs you were working in. They close during the move.
## 2. Look at what you are about to publish
Everything in the folder reaches whoever clones it. Walk the workspace once before the first commit:
* **Locked notes** travel as ciphertext and stay shut, so they cost nothing to publish ([Note Locking](https://ledge.sh/docs/note-locking)).
* **Profiles** are outside the folder already, so no credential in one can reach the repository ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)).
* **`env:` lines** sit in a note's frontmatter in the open and publish as written. A value that should not travel belongs in a profile ([Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments)).
* **The trash** stays out of git on its own. Ledge puts a `.gitignore` inside `.ledge-trash/` that covers the whole folder.
## 3. Make the repository
In the workspace folder:
```sh
git init
git add -A
git commit -m "notes"
```
A note in an attached workspace runs its blocks in the folder itself, so keep those lines in a note in the workspace ([Running Code](https://ledge.sh/docs/running-code)).
A workspace Ledge created arrives with a `.gitignore` for `.DS_Store` and the temp file a save leaves behind. A folder you made yourself has neither line, so add them:
```
.*.md.tmp-*
.DS_Store
```
## 4. Push it somewhere everyone reaches
Make an empty repository on GitHub, a self-hosted forge, or any machine everyone has ssh to:
```sh
git remote add origin git@github.com:you/notes.git
git push -u origin main
```
Make it private unless the notes are meant to be public. Whoever can read the repository can read the notes.
## 5. Clone it everywhere else
Everyone else clones the repository to a folder of their own:
```sh
git clone git@github.com:you/notes.git ~/Projects/notes
```
Then "Attach Folder as Workspace…" in their own Ledge, pointed at the clone.
It becomes an ordinary attached workspace, with its own row in the strip and its own number in ⌘1 through ⌘9. The notes, the images, and the folders you filed them in are all there.
## 6. Exchange changes
Each side pushes what it wrote and pulls what the others did:
```sh
git pull --rebase
git add -A
git commit -m "notes"
git push
```
Keep those four lines in a note in the workspace and the exchange is one ⌘↩.
Ledge follows the folder while you work. Notes appear, change, and disappear in the sidebar as a pull lands, with nothing to refresh and nothing to close first.
## 7. Settle a note two people changed
Notes are separate files, so work on different notes merges cleanly and never reaches this step.
One note changed in two clones is a git conflict like any other. The pull leaves the conflict markers in the file and Ledge shows them in the note. Edit them out, save, then commit.
A pull that arrives while you have that note open and edited is settled by Ledge first. Your version keeps the file, and the incoming one goes to the workspace trash with a notice in the sidebar naming it. Restoring it from the trash puts that copy beside the live note, so you can merge the two yourself.
## 8. Read a pull before you run it
A block runs on the machine that presses Run. Read what arrived before you run it, the way you would read a script from the same person.
Expect some of it not to fit. A note's `cwd:`, `host:`, and `profile:` lines name paths, machines, and credentials on the setup it was written for, so a block that deploys from your laptop may find none of that in anyone else's clone ([Frontmatter and Environments](https://ledge.sh/docs/frontmatter-and-environments)).
## Where to go next
* **Automate the exchange.** The four lines in step 6 run from cron through the CLI, so a clone keeps itself current ([The ledge CLI](https://ledge.sh/docs/the-ledge-cli)).
* **Keep your own notes out of it.** A shared workspace is one workspace. Notes that are yours alone belong in another, and the strip switches between them ([Notes and Workspaces](https://ledge.sh/docs/notes-and-workspaces)).
* **Share a machine instead of a folder.** One live copy instead of a clone each is a server. It comes with one account shared by everyone on it ([Keep Notes on a Remote Server](https://ledge.sh/docs/keep-notes-on-a-remote-server)).
* **Send a secret another way.** Profiles never enter a notes folder, so everyone keeps their own copy of the credentials a shared note names ([Profiles and Secrets](https://ledge.sh/docs/profiles-and-secrets)).