Repository navigation
Skiff agent
The Skiff agent is one agent session in a folder of its own. It sends tasks to sessions in your Skiff projects, follows them, and reports the results to you. Its default name is Skipper.
Turn it on in Settings › Skiff agent. Settings also selects the agent it runs, its name, and whether it continues its last conversation after a restore.
- In the app, select its row above the projects.
- From any terminal, run
skiff skipper. This includes an SSH login.
skiff skipper attaches to the Skiff agent's session. If the session does not run, the command starts it first. If skiffd does not run, the command starts skiffd too. Press Ctrl+\ to detach. The agent keeps running. See CLI for each step.
skiffd marks the Skiff agent's session. The mark stays in the workspace through a reload and a restore. The app and the CLI find the session by this mark. Two opens at the same time start one session.
A session that an older Skiff started has no mark. On the first open, Skiff adopts it if it is a live agent session with the configured name in the Skiff agent's folder.
If the session is a bare shell, an open starts the agent in it again. This occurs after a restore, or after the agent quit. When Settings continues on restore, the agent continues its last conversation in the folder:
| Agent | Line |
|---|---|
| Claude Code, opencode, Grok, Gemini | <command> --continue |
| Codex | <command> resume --last |
| A custom agent | No continue line. A new conversation starts. |
For 15 seconds after Skiff types a start line, a second open types nothing.
The default folder is ~/skiff-agent. Settings › Skiff agent › Folder shows the path, with Copy path. A line under the path shows only when the folder cannot be used.
To use a different folder, select Change… in Settings › Skiff agent › Folder, or run skiff skipper --move-to PATH. Add --dry-run to see what the command would do. Both end the Skiff agent's session first, because it works in the old folder. You can also set folder under [skiff_agent] in projects.toml by hand.
The folder at the new path decides what Skiff does:
| The folder | Result |
|---|---|
| New or empty | Skiff copies the current folder into it, or starts a new ledger. Skiff checks the copy before it switches. |
A Skiff folder (it has the .skiff-agent marker) |
Skiff switches to it as it is. Skiff does not copy and never merges two ledgers. |
| Any other folder | Skiff refuses it, with one line that tells why. |
The old folder keeps every file and gets MOVED.md. Skiff never deletes it. If the change fails before Skiff writes projects.toml, the config and the old folder stay as they were. The conversation does not move: the agent starts a new one in the new folder.
Skiff refuses a folder that is the root, the home folder, in the config folder, a project or in or above a project, or in a Git repository. An agent reads instruction files in the folders above its own, so these folders would mix with other work. Skiff also refuses an agent's own home (such as ~/.claude or ~/.codex) and a Hermes profile, because Skiff does not write in an agent's setup. A dot-folder such as ~/.skiff-agent is a usual folder.
Setup writes only the parts that are missing. It keeps your edits. The folder must be new, empty, or one that setup made before.
| Path | Content |
|---|---|
AGENTS.md |
The agent's instructions, with its name. Edit this file. |
CLAUDE.md, GEMINI.md
|
Links to AGENTS.md. |
.agents/skills/skiff-cli |
A link to Skiff's copy of the skiff-cli skill. |
.claude |
A link to .agents, for Claude Code's skills. |
results/ |
Result files from child sessions. |
ledger.db |
The task ledger. Skiff makes it on first use. |
.skiff-agent |
The marker that tells setup the folder is the agent's. |
Skiff keeps its copy of the skill in skills/skiff-cli beside projects.toml. Each open replaces the copy when the skill built into Skiff is different.
The ledger is the agent's record of tasks. It is a SQLite file, ledger.db, in the agent's folder. SKIFF_LEDGER sets a different path. The agent reads and changes it only with skiff ledger. See CLI for the commands and options.
Each row has a row ID and an 8-character uid. Skiff never gives either one again, also after rm. Each command that takes an ID also takes the uid.
| State | Meaning |
|---|---|
sent |
The task went to the pane. |
pending |
The task is not delivered yet. For example, the pane was working. skiff ledger send sends it. |
blocked |
The pane needs the person. ledger send sets it when the text went in but no turn started. |
done |
The task is complete. |
interrupted |
Shown only, never stored. The pane restarted after the send. ledger send can send the task again. |
gone |
Shown only, never stored. The task is open, and its pane no longer exists. |
Each pane has a process generation: the start time of its process. A reload keeps it. A restore runs a new shell under the old pane ID, so the generation changes. ledger add stores the generation. ledger send stores it again after a send, and ledger update when it sets the state sent or a new pane. When the generation of a sent or blocked task is old, ls and show print interrupted.
A plan is a row with no pane. Use --parent to put tasks under it. ls shows each plan's tasks under it. A plan with tasks shows done when every task is done, and sent while one is open. The watcher ignores plans.
skiff ledger add --pane PANE --task TEXT --send --enter adds a task and sends it in one step. skiff ledger send UID --enter sends a pending or interrupted task. Both send the task text with its result path, run every skiff send check, and exit with the same code. The task becomes sent on exit 0 and blocked on exit 8. A refusal leaves it as it was. See CLI.
--ref stores free text, such as a URL or a ticket key. Skiff does not read it. The ledger does not need a tracker, git, or a network.
When add gets no --result, it stores and prints a path in results/ beside the ledger. The date is today's date in local time. The slug is the task text in lowercase ASCII words joined by -, at most 40 characters.
| Row | Path |
|---|---|
| A task with no plan | results/<date>-<uid>-<slug>.md |
| A plan | The folder results/<date>-<uid>-<slug>/, with plan.md in it |
| A task in a plan |
<uid>-<slug>.md in the plan's folder |
add makes no file. rm deletes no file.
- An old
ledger.mdbeside the ledger is imported on first use. Skiff keeps it asledger.md.bak. - A version 1 ledger changes to version 2 on first open, in one transaction. Each row keeps its ID and gets a uid.
skiff watch follows the panes of the open tasks in the ledger: rows in the state sent, pending, or blocked. Skiff starts it when it starts or finds the Skiff agent's session. You do not start it yourself.
The agent icon on the Skiff agent's row shows the watcher. While the watcher runs, the icon is teal. It turns slowly only while the agent is idle and another session works or waits: the turn means the watcher has work to watch. With nothing to watch, the icon is teal and still. When no watcher runs, or its last line failed, the icon is red and still. With reduced motion, the icon does not turn.
| The pane | Result |
|---|---|
| Waits for an approval or rings the bell | You get a desktop notification. The agent gets nothing. |
| Ends a turn | The agent gets a line. |
| Goes idle after a prompt, with no turn | The agent gets a line. |
Is idle with a pending task |
The agent gets a line that names the oldest pending uid and the command that sends it. This occurs once for each idle stretch. |
| Exits, or its agent exits | The agent gets a line. |
| Is gone | The agent gets a line. |
| Restarted after the send | The agent gets a line that names the task's uid as interrupted. This occurs once for each watcher run. The watcher then stops following that pane for the task. |
A line looks like this:
[watch] pane reviewer 3f2a9c1b is idle. Read it, then mark it seen.
A line for a pending task looks like this:
[watch] pane reviewer 3f2a9c1b is idle: task 7c2e91ab is pending: skiff ledger send 7c2e91ab --enter. Read it, then mark it seen.
Lines that wait are joined into one line. A newer fact for a pane replaces an older one. An idle line is dropped when its pane works or waits again.
skiffd sends an event at the end of each turn. The watcher uses this event, so a rename, a theme change, or a resize does not wake the agent. A pane stays unread until the agent or the person marks it seen. So a new watcher run tells the agent again.
The watcher types a line only when all of these are true:
- The agent's session is idle.
- A known agent is in front. A shell in front holds the line.
- The agent's input box shows and is empty. A draft holds the line.
- No busy line, such as "esc to interrupt", shows.
Just before it types a line, the watcher reads the ledger again and drops a pending line whose task is no longer pending. ledger send refuses a task that is not pending, so a row that changes after that read is safe.
The watcher types the line and presses Enter in one request. skiffd refuses the request if the person typed in the pane after the watcher checked it. Then the line stays held. Keys that arrive during the Enter delay wait until after the Enter.
After a wake, the watcher waits for a turn to start. If no turn starts, it logs the wake as undelivered. It does not press Enter again, because that could submit the line twice. The line can stay in the input box.
- One watcher runs for each daemon. A lock beside the socket, such as
skiffd.watch.lock, enforces this. - The watcher logs one line for each action to a file beside the socket, such as
skiffd.watch.log. - It continues after an app restart.
- After a daemon reload or restart, it connects again. It stops if
skiffddoes not come back within 10 minutes. - It stops when the Skiff agent's session ends.
- It keeps no state of its own. The ledger, the session list, and each pane's unread flag are its input.
-
--no-notifyturns off desktop notifications. Notifications usenotify-sendon Linux andosascripton macOS. If the tool is missing, the log line is the only record.
A daemon or a watcher that skiff skipper starts from an SSH login must not belong to that login. If it did, a logout could stop it, and programs in its sessions would lose the login's session keyring.
On Linux with a systemd user manager, Skiff starts each one through systemd-run --user --scope:
| Process | Unit |
|---|---|
skiffd |
skiffd-<socket name>, for example skiffd-skiffd.scope
|
skiff watch |
skiff-watch-<socket name> |
Both get DISPLAY, WAYLAND_DISPLAY, DBUS_SESSION_BUS_ADDRESS, and XDG_RUNTIME_DIR from systemctl --user show-environment, so their sessions can use the desktop. Without a user manager, they start without a scope.
Both also get a session keyring of their own, linked to the user keyring, as pam_keyinit does.
The daemon that skiff skipper starts writes its log beside the socket, as the app does.