Your agents / Other runtimes
Other runtimes
Handsoff has adapters for Claude Code and Codex. This page is for everything else: an agent runtime with no adapter, and a runner. A runner is a program that starts agent sessions for someone, often with no person there.
Before you start
The examples use the workspace billing from Your first relay, with its agent handle build-agent and its baton file baton.md. The runner's session is called run-0044. The section "Try it by hand" opens the work that this session holds, BILL-64. Work names are used once in a workspace, so if BILL-64 already exists in yours, use a new name.
An agent in any runtime
Any agent that can run a shell command can carry work. It needs three things:
- The
handsofftool on its PATH, on a machine where a person signed in. Install and Sign in show how. - The standing instruction in its system prompt. Tell your agents shows the text and where it goes.
- A handle to act as, in
HANDSOFF_HANDLE.
The agent then opens, saves, offers, catches and ends work with the same commands as everyone else.
Without an adapter, nothing runs at the start or end of a session. So:
- Name the work when you start the agent, such as "Continue the work named BILL-42." Nothing prints the status for it at the start.
- The agent must keep its lease alive. Each accepted save renews the lease. Between saves,
handsoff renew <work>renews it. - The agent must save, then offer or end its work before it stops. If it stops without doing so, its lease runs out and the work drops.
What a runner does
A runner starts sessions and sees them end. Handsoff asks it to do a little before each session and one thing after. In return, the history says why each session ended, such as "ended with reason limit", and not only that a lease ran out.
A runner talks to Handsoff only through the handsoff tool. It never reads a session's transcript, and it never writes a baton itself.
1. Sign the runner in once
Give the runner its own machine account and its own account at your team's sign-in page. Do not share a sign-in account with a person or with another runner. Some sign-in services end every sign-in of an account when one refresh is used twice.
A person signs the runner's account in once, on the runner's machine, without a browser:
HANDSOFF_CONFIG_DIR=<runner config dir> HANDSOFF_STATE_DIR=<runner state dir> handsoff login --server https://handsoff.run --no-browser
Open this address on any device:
<sign-in address>
Paste the final address the browser was sent to:
Signed in as sx41aCJJW3wG5zsTQbLyRknnu7tvvGxx on https://handsoff.run
Sign in explains the paste-back steps. The tool keeps the runner's tokens in that config folder and renews them. Sessions never sign in.
Then the workspace owner gives the runner a handle. If the runner owns the workspace, it adds one with handsoff handle add <name> --kind agent --mine. If not, the owner sends it a claim code. Workspaces shows both ways.
If the tool later says a person must run handsoff login, the sign-in service ended the sign-in. Sign the runner in again.
2. Before each session
Choose the session's id before the runtime starts. Start the runtime with that id if it lets you. For Claude Code that is --session-id <id>, and --resume <id> for a later process of the same session. The hooks read the runtime's own id, so the two must match.
Set these variables in the session's environment:
| Variable | Value |
|---|---|
HANDSOFF_HANDLE | The handle the session acts as. |
HANDSOFF_SESSION | The session's id, as above. |
HANDSOFF_RUNTIME | The runtime's name, such as claude-code or codex. |
HANDSOFF_RUNNER | Your runner's name. It tells the hooks to leave the end to you. |
HANDSOFF_MODEL | The model, when you know it. |
HANDSOFF_CONFIG_DIR, HANDSOFF_STATE_DIR | The runner's sign-in and state folders. |
HANDSOFF_SERVER, HANDSOFF_WORKSPACE | Optional defaults for the tool. |
These values belong to the runner. If the person who asks for a session can also set variables, replace their HANDSOFF_ values with yours and drop any other HANDSOFF_ name. Otherwise they could point the runner's sign-in at another account or service.
Put the folder that holds handsoff first on the session's PATH. The hooks call the bare command handsoff.
For Claude Code, give the session the hooks without writing a file into its project. --print prints them as one JSON object:
handsoff adapter install claude-code --project . --print
{"hooks":{"PostToolUse":[{"hooks":[{"async":true,"command":"handsoff hook claude-code renew","timeout":3,"type":"command"}],"matcher":"*"}],…
Pass it to Claude Code with --settings '<json>'. If you already pass other settings for the session, merge them into the same object, and pass --settings once.
Any other runtime name is refused, because Handsoff has adapters for these two only.
3. After the session's process exits
Each time a session's process exits, read the runtime's final result. Then run handsoff session end with the same config and state folders the session had. This ends a leg only when the session holds one. Here session run-0044 has not opened any work yet, so it holds no leg:
handsoff session end --session run-0044 --reason limit --cause "usage limit reached"
No live leg matched this session.
The tool says so and exits with code 0. That is not an error. You see this, for example, before a session has opened any work, or when you run session end twice. When the session holds a live leg, the command ends every live leg the session holds and records the cause on each. A leg is one carrier's stretch on the work. "Try it by hand" below ends one.
Choose the reason from the runtime's own success or error report. Never use the process's exit code, and never guess from an earlier stop.
| Reason | When |
|---|---|
killed | You stopped the process yourself while it was working. |
limit | The result reports a usage or rate limit. |
crash | The result reports any other failure. For Claude Code, that is is_error: true. Use its terminal_reason and api_error_status as the cause. |
clean | The result reports success. |
other | You cannot tell, for example because there was no result at all. |
For Claude Code, read is_error, not subtype. A failed run can report subtype: "success" with is_error: true. If one process runs many turns, use the result of the last turn you started. Put only the error fields in the cause, never the result text or the prompt.
The tool checks a clean for you. If the Claude Code or Codex hooks counted tool use since the last save, or the tool cannot read that count, it records other with a note instead.
If you start a new process for the same session later, run session end for the old process first. The new process holds nothing at first. It sees its dropped work in the status at session start and catches it again with handsoff continue.
If session end fails, log it and carry on. A runner that never calls it leaves the work to drop when its lease runs out.
Try it by hand
You can play both parts in one shell. Here the exports stand in for the runner, and the open stands in for the agent:
export HANDSOFF_HANDLE=build-agent HANDSOFF_SESSION=run-0044 HANDSOFF_RUNTIME=my-runtime HANDSOFF_RUNNER=my-runner HANDSOFF_MODEL=my-model
handsoff open BILL-64 --title "Invoice export check" --baton baton.md
Opened BILL-64 at r1 (work 75)
Leg 139; lease expires 2026-10-05T04:55:04.204419+00:00
Then end the session as a runner would. The session now holds BILL-64, so the command ends its leg:
handsoff session end --session run-0044 --reason limit --cause "usage limit reached"
Ended billing / BILL-64 (leg 139): limit
The history shows what the runner reported:
handsoff history BILL-64
…
Leg 1: build-agent (agent, unknown)
opened at r1; saved r1
runtime my-runtime and model my-model (as reported); session run-0044
ended with reason limit at 2026-10-05T04:25:04.679782+00:00
2026-10-05T04:25:04.20943+00:00 opened: {"reference":"BILL-64"}
2026-10-05T04:25:04.679129+00:00 runtime_event: {"cause":"usage limit reached","kind":"session_end"}
2026-10-05T04:25:04.680353+00:00 dropped: {"end_reason":"limit","reason":"ended without a hand-over"}
2026-10-05T04:25:04.680926+00:00 ended: {"note":null,"reason":"limit"}
The work dropped, so the next carrier gets a warning when it catches it. To hand the work over cleanly, the agent offers it before the process exits.
The two commands, as the help prints them
handsoff hook is the command the Claude Code and Codex hooks run. The runtime calls it, with the event on standard input. You do not run it yourself:
handsoff hook --help
Usage: handsoff hook [OPTIONS] <RUNTIME> <EVENT>
Arguments:
<RUNTIME>
<EVENT>
…
handsoff session end is the command a runner runs after each process exits:
handsoff session end --help
Usage: handsoff session end [OPTIONS] --reason <REASON>
Options:
--reason <REASON> [possible values: clean, limit, crash, killed, other]
--server <SERVER>
--cause <CAUSE>
--workspace <WORKSPACE>
--as <HANDLE>
--note <NOTE>
--session <SESSION>
--runtime <RUNTIME>
--model <MODEL>
--leg <LEG>
--account <ACCOUNT>
--json
-h, --help Print help
Checklist
- The runner has its own machine account and its own sign-in account, signed in once.
- With Handsoff not set up, the runner starts sessions exactly as it did before.
- Each session gets the variables above, the tool first on its PATH, and the printed hooks.
- Nothing is written into the session's project.
- Each process exit is followed by one
session end, with a reason from the final result. - The runner logs every failure of the tool, and none of them fails the session.