Hand a task off and keep working: a background run works by itself in its own git worktree and branch, survives closing the terminal, and waits for you if it needs a decision.
chanto run --bg "add retries to the HTTP client" # returns at once with the run id
chanto runs # id, state, duration, cost, verify result, branch, task
chanto attach 3f2a9c1d # follow it live, answer prompts; Ctrl-C detaches
chanto runs show 3f2a9c1d # summary, diff stat, last messages
chanto runs apply 3f2a9c1d # bring its commits onto your current branch
chanto runs apply 3f2a9c1d --pr # or push the branch and open a PR with gh
chanto runs stop 3f2a9c1d | resume 3f2a9c1d | rm 3f2a9c1dIn the TUI, start a prompt with & (or use /bg <task>); /runs lists runs, /runs <id> shows one, /runs attach <id> mirrors its messages into your session and shows its permission prompts as the usual dialog (/runs detach stops following). Ids can be shortened to a unique prefix. -m/--model, --provider, --base-url, --mode, --max-cost and --no-verify apply to the run.
The daemon. Runs are hosted by the Chanto server (chanto serve), started on demand in the background (detached from the terminal; log in ~/.chanto/daemon.log) and found through ~/.chanto/server.json, like any chanto serve. chanto daemon status shows it and its runs, chanto daemon stop stops it and chanto daemon start starts it. Each run is its own worker process, so runs in different repositories don't share state. Run records and their event logs live in ~/.chanto/runs/<id>/ (run.json, events.jsonl, worker.log), readable only by you (on Unix, runs/ and its directories are mode 700, the files 600), with secrets masked in the event log. Stopping the daemon (or a crash) leaves active runs interrupted: their sessions are saved, and chanto runs resume <id> continues one from where it stopped. A server that listens beyond this machine (--host, --tailscale) hosts runs only with --runs.
Isolation. A run works in its own git worktree, ~/.chanto/runs/worktrees/<project>/<id>, on a new branch chanto/run/<id> created from your current HEAD. The worktree is outside your project, so test runners, build globs, indexers and file watchers never see a second copy of it. It is trusted (project settings, hooks, MCP servers) exactly when the source repository is, and its own .chanto/ settings stay protected from the agent. Your uncommitted changes (staged, unstaged and untracked files, minus ignored ones) come along as a first commit on that branch, "Uncommitted changes at the start of background run git add -A into a temporary GIT_INDEX_FILE, then write-tree and commit-tree), so your working copy, index and stash are never touched. When the run finishes, whatever it left uncommitted is committed to its branch. chanto runs rm removes the worktree and branch (and prunes git's worktree list). Directories that aren't in a git repository (or have no commits) are refused with a message saying so.
Applying. chanto runs apply <id> cherry-picks the run's own commits (everything after the snapshot) onto the branch you have checked out. If your working tree has uncommitted changes, it applies the run's diff to the working tree instead and leaves it uncommitted; if that doesn't apply cleanly, nothing changes and you're told to commit or stash first. A conflicting cherry-pick is aborted, leaving the exact git cherry-pick range to run by hand. --pr pushes the branch to origin and runs gh pr create (it only happens when you ask for it).
Autonomy and limits. Runs default to acceptEdits with shell commands in the OS sandbox: edits inside the worktree and commands that stay in the sandbox go ahead; anything else (leaving the sandbox, secret files, protected paths, dangerous commands) pauses the run in state needs-input until someone answers with chanto attach, /runs attach or the API. Prompts nobody answers are denied after input_timeout_minutes, so a run never waits forever. Verified mode is on (its result shows in chanto runs as passed, failed, skipped or off); the run stops at its cost budget or its wall-clock limit (time spent waiting for input doesn't count), and at most max_parallel runs work at once while the rest wait as queued.
[background]
mode = "acceptEdits" # permission mode for runs
allow_sandboxed_commands = true # acceptEdits: sandboxed commands run without asking
sandbox = true
verify = true # `--no-verify` per run
max_cost_usd = 2.0
max_minutes = 60
input_timeout_minutes = 1440
max_parallel = 2
# model = "qwen3-coder" # default: your configured modelNotifications. When a run finishes, fails or needs input, Chanto can tell you:
[notify]
ntfy = "https://ntfy.sh/my-long-random-topic" # plain HTTP POST; subscribe in the ntfy phone app
webhook = "https://example.com/chanto-hook" # JSON POST: {event, run_id, state, title, message, url}
desktop = true # macOS Notification Center / Linux notify-send; default on for the local daemon
link = "http://my-mac.tailnet-name.ts.net:4141" # where notification links point (default: the server)A notification has only a title, the run id, its state, the verify verdict and a link to the run (/#run=<id>). It never includes the task, code, output or error messages, because those don't belong in notification services or on lock screens. Treat an ntfy topic name like a password, or use your own ntfy server.
API. The same server, behind its bearer token (see Server), so schedulers and scripts can drive runs:
| Route | |
|---|---|
POST /runs | {task, cwd, model?, provider?, base_url?, mode?, verify?, sandbox?, max_cost_usd?, max_minutes?, start?, kind?, team?} → the run (201). source: {"kind": "local", "path": ...} may replace cwd. start: a commit to begin at; kind: "goal"; team: the team it belongs to. |
GET /runs | {runs: [...]}, newest first |
GET /runs/{id} | the run plus diff_stat, commits, last_messages, last_seq |
GET /runs/{id}/events?since=<seq> | server-sent events: the logged events after since, then live ones; ends once the run reaches a final state |
POST /runs/{id}/respond | {request_id?, decision: "allow" | "allow_always" | "deny", feedback?} or {request_id?, answer} for a question |
POST /runs/{id}/stop, POST /runs/{id}/resume, DELETE /runs/{id} | stop, continue an interrupted run, remove its worktree, branch and record |
POST /runs/{id}/apply | {pr?}: apply a run of a local checkout onto that checkout's current branch, or push and open a PR |
POST /runs/bundles, GET /runs/{id}/bundle | upload a git bundle as a run's source (source: {kind: "bundle", bundle_id, ref, …}; a {kind: "git", url, ref} source is fetched instead), and download a run's commits |
A run's state is queued, running, needs-input, done, failed, cancelled or interrupted; pending holds the open permission request or question. Events are the session events (text, tool_start, permission_request, …) with a seq and run_id, plus run_state (with the whole run record) on every state change and request_done when a prompt is answered or times out.
Starting point. start (API) makes a run begin at a given commit of the local checkout instead of HEAD plus your uncommitted changes. Teams use it to start every sub-task from the same base, and to start a re-delegated one from the merged work. kind: "goal" runs a goal, and team tags a run as part of a team.
Container isolation
By default a run is isolated by its own worktree and the OS sandbox (Seatbelt, Landlock, or a write-restricted token on Windows). With isolate = "docker" (or "podman") its shell commands run in a container instead:
[background]
isolate = "docker" # or "podman"; "none" (default) keeps the OS sandbox
image = "rust:1-bookworm" # default: by project type (see below)
network = "none" # none | allowlist | full
cpus = 2 # optional limits
memory = "4g"How it works. The agent loop stays on the host, and each run gets one long-lived container (chanto-run-<id>) with its worktree bind-mounted at the same path. Every command the model writes (Bash, background shells, and verified mode's checks) is a docker exec into it, so cd, paths in output and paths in error messages mean the same thing on both sides. Chanto's own file tools (Read, Edit, Write) work on the host worktree, and the bind mount makes their changes visible in the container. The alternative, running the whole worker inside the container, would need a Linux build of Chanto (not available on a macOS host) and would have to put your model credentials into the container. With this design they never enter it: a command sees only the image's environment plus CHANTO=1, the pager/color settings Chanto always sets and, with allowlist, its proxy variables. Nothing from your shell environment is passed, so no provider keys and no secret-named variables. Nothing else from the host is mounted: no home directory, no SSH keys, no Docker socket. The repository's .git directory is mounted read-only, so the container can't plant hooks or config that your own git would later run. Only the worktree's own git metadata (index, HEAD) is writable. Git commands that read work if the image has git, but git add/git commit inside the container fail on the read-only object store. The run's changes are committed on the host when it finishes, as usual. On Linux the container runs as your user (so files in the worktree stay yours, with HOME set to a scratch directory). With Docker Desktop on macOS it runs as root, and file ownership is mapped by Docker Desktop. Containers get --pids-limit 4096 and no-new-privileges.
Images. Without image, Chanto picks one by project type: rust:1-bookworm (Cargo.toml), golang:1-bookworm (go.mod), node:22-bookworm (package.json), python:3.12-bookworm (pyproject.toml, requirements.txt, setup.py), ruby:3-bookworm (Gemfile), eclipse-temurin:21-jdk (Maven/Gradle), else buildpack-deps:bookworm. The engine pulls an image the first time it's used, which can take a while. These images are large, so set image to something smaller you already have if disk space matters.
Network. none (default): no network at all. full: the engine's default bridge. allowlist: the container sits on an internal network (--internal, no route out) whose only other member is a small relay container (alpine:latest running busybox nc). The relay forwards to Chanto's filtering proxy on the host, and commands get HTTP(S)_PROXY pointing at it, so only [sandbox] allowed_domains (default: the package registries) minus denied_domains are reachable. A blocked domain is offered for approval like any sandboxed command's. Tools that ignore proxy variables simply get no network. With Docker Desktop the relay reaches the host's loopback proxy through host.docker.internal. On Linux Chanto also serves the proxy on the bridge's gateway address.
Permissions. In acceptEdits (the default for runs), commands that run in the container go ahead without asking when allow_sandboxed_commands = true, except for the usual dangerous ones. dangerouslyDisableSandbox has no effect: the container is the boundary.
Lifecycle. The worker creates the container when the run starts (or resumes) and the daemon removes it, together with the relay and network, when the run ends, is stopped or is removed. On start, the daemon also removes containers that a crashed daemon left behind, which it finds by their chanto.store label. Run creation fails at once with a clear message if the engine isn't installed or isn't running ("start Docker Desktop"). chanto runs show shows the isolation in use (Isolation: container (docker · rust:1-bookworm · network none; chanto-run-3f2a9c1d)), and docker ps --filter label=chanto.run lists the live containers.
These settings live in ~/.chanto/config.toml, or in a trusted project's .chanto/config.toml ([background] is ignored in projects you haven't trusted).
Remote runs: your own server
Run tasks on a machine you control (a VM, a desktop with a GPU) and keep your laptop free:
# on the server
chanto serve --runs --tailscale # or --host 0.0.0.0 behind your own TLS proxy
cat ~/.chanto/server.json # url and token (or pin the token with CHANTO_SERVER_TOKEN)
# on your laptop
chanto remote add box http://box.tailnet-name.ts.net:4141 --token <token> # saved in ~/.chanto/auth.json (0600)
chanto run --bg --on box "add retries to the HTTP client"
chanto runs --on box
chanto attach 3f2a9c1d --on box
chanto runs apply 3f2a9c1d --on box # or --pr
chanto remote list | chanto remote rm boxAll run commands take --on <server>. How the code gets there:
- Git bundle (default). Chanto bundles your HEAD plus a snapshot of uncommitted changes (the same snapshot commit as local runs) and uploads it (
POST /runs/bundles, limit[background] max_bundle_mb, default 1024). Nothing is pushed anywhere. - Shared git remote. With
[remote.box] git_remote = "origin"and a clean working tree, Chanto pushes your current branch to that remote (a normal, non-force push) and the server fetches it from the same URL, so the server needs read access to it. With uncommitted changes or a detached HEAD, it falls back to a bundle.
The server fetches into a bare repository under its own ~/.chanto/runs/repos/, adds the run's worktree under ~/.chanto/runs/worktrees/, and runs it exactly like a local run. Code that came from another machine is never trusted there: its project settings, hooks and MCP servers are ignored, and the server's own [background] settings apply. chanto runs apply --on downloads the run's commits as a git bundle (GET /runs/{id}/bundle) into your checkout as the branch chanto/run/<id>, then applies them like a local run. The server's answer is checked first: the branch must be the run's own chanto/run/<id>, its base a full commit hash that your checkout has below the run's work, and an existing branch of that name is never moved (delete it to fetch the run again); --pr pushes that branch from your machine with your credentials and opens the PR with gh.
Deploying on a VM. Install Chanto, configure a model (for example [providers] pointing at a hosted API, or Ollama on the same box), and run it as a service:
# /etc/systemd/system/chanto.service
[Unit]
Description=chanto run server
After=network-online.target tailscaled.service
Wants=network-online.target
[Service]
User=chanto
Environment=CHANTO_SERVER_TOKEN=change-me-to-a-long-random-string
ExecStart=/usr/local/bin/chanto serve --runs --tailscale --port 4141
Restart=on-failure
# Runs need git, and the OS sandbox needs Landlock (Linux 5.13+).
[Install]
WantedBy=multi-user.targetsystemctl enable --now chanto. Pin the token with CHANTO_SERVER_TOKEN so clients survive restarts. If the service restarts while runs are active, they come back interrupted; chanto runs resume <id> --on box continues them. Reach the server privately over Tailscale: --tailscale binds only to the tailnet address, so nothing outside your tailnet can connect, and the bearer token is a second layer. Use --host 0.0.0.0 only behind a TLS-terminating proxy you trust.
Stronger isolation. Runs on a server can use container isolation like local ones; for more, run the server itself in a VM.
Queue runners: taking runs from Chanto cloud
chanto runner connect turns a machine into a runner for a run queue (Chanto cloud, or any server that implements the runner protocol in proto/src/runs.rs). The runner only makes outbound HTTPS requests, so it works behind a firewall.
chanto runner connect https://cloud.example --registration-token hrr_… # once: registers, saves its token
chanto runner connect https://cloud.example # afterwards
chanto runner connect https://cloud.example --label gpu --label linux # only runs that ask for these
CHANTO_CLOUD_URL=… CHANTO_RUNNER_REGISTRATION_TOKEN=… chanto runner connect --ephemeral # in a throwaway VM
chanto runner connect https://cloud.example --own-model # always use this machine's model setup
chanto runner connect https://cloud.example --allow-scripts # also run the scripts jobs come withScripts. A job can come with a shell script that runs before (or instead of) the model. It is the server's code, so a runner only runs it with --allow-scripts (CHANTO_RUNNER_ALLOW_SCRIPTS=1); without that, such a job is reported failed with a note saying so. Allowed scripts run like the agent's Bash commands: in the sandbox under the machine's [sandbox] settings, without its secret-named environment variables (the run's own org secrets are passed), in a fresh checkout whose git directory is kept under ~/.chanto/runner/pulled/scripts/, out of the script's reach. A script that runs git commit itself therefore fails in the sandbox; its changes are committed for it.
Credentials. For each job the runner asks the server for the run's credentials. If the server sends a git credential (Chanto cloud does for connected GitHub, GitLab and Bitbucket repositories), the runner clones and pushes with it; otherwise it uses the machine's own git credentials. If the server offers a model endpoint (Chanto cloud's model gateway, or one the org set up), the run uses it; otherwise, or with --own-model (CHANTO_RUNNER_OWN_MODEL=1), it runs with the machine's Chanto configuration, so a local or private model's key never reaches the server. The org's secrets are passed to the run's shell commands as environment variables and masked like any other secret; names starting with CHANTO_ are skipped. --name sets the name the runner registers with, and --mode the permission mode for its runs.
The registration token comes from the org's runner settings; the runner's own token is saved in ~/.chanto/runner/connections.json (0600). Each job is cloned into a cached bare repository under ~/.chanto/runner/pulled/ and run as an ordinary background run of a remote source (untrusted, like remote runs), with [background] settings, verified mode and container isolation as configured. Its events stream to the server as they happen. When it finishes, its branch chanto/run/<id> is pushed to the repository and reported with the run's summary, and the worktree is removed.
Nobody watches these runs, so permission prompts are denied with an explanation and questions are answered with "use your best judgment"; choose [background] mode (or --mode) with that in mind. The runner keeps its job with a heartbeat. If it stops (Ctrl-C, crash, lost network), the server gives the run to another runner once the lease runs out; a cancel from the server stops the run at the next heartbeat. --ephemeral takes one job and deregisters, for runners in VMs or containers that are thrown away afterwards; --once takes one job and exits.
Cloud runs from the CLI
Sign in to Chanto cloud with GitHub, then send a task from your terminal, follow it there, and get its branch back:
chanto cloud login # browser sign-in; the token goes in ~/.chanto/auth.json (0600)
chanto cloud status # who you are, your orgs, where runs go
chanto cloud org acme # make acme the default org (--org picks one per command)
chanto run --cloud "add retries to the HTTP client" # queue it, then follow it here; Ctrl-C detaches
chanto run --cloud --placement hosted --label gpu "…" # placement auto | hosted | self-hosted; labels pick runners
chanto runs --cloud # the org's recent runs (--status, --org, --limit)
chanto attach 0b9e6f1c --cloud # follow one again (ids can be shortened)
chanto runs show 0b9e6f1c --cloud | chanto runs cancel 0b9e6f1c --cloud
chanto cloud logout # revokes the token on the server and forgets itFrom the agent. Signed in, the agent also gets a ChantoCloud tool, so you can ask in plain words ("send this to the cloud", "how's my cloud run doing?", "run the docs agent on issue 42"). It lists runs and agents and shows a run's result freely; starting a run (of the current branch, which must already be pushed), cancelling one or starting an agent asks you first, like any other action with outside effects. Signed out, the tool isn't offered.
What the run starts from. A runner clones your repository from its origin remote (an SSH remote is sent as its HTTPS URL, and credentials in a remote URL are never sent), on the branch you have checked out. Work it can't see is refused rather than silently left out: with uncommitted changes Chanto asks you to commit or stash them, and with commits that aren't on origin (or a branch that was never pushed) it asks you to push. --push pushes the branch for you (a normal, non-force git push -u origin <branch>) when the working tree is clean. A detached HEAD is refused too.
Following. chanto run --cloud prints the run's id and follows it: the model's text, its tool calls and failures as they reach the server, whether it's waiting for a runner or running on a hosted or self-hosted one, and finally its summary. Cloud runs are unattended, so permission prompts are denied and questions answered by the runner (see above); you see them in the stream. --detach returns as soon as the run is queued. The exit code is 0 when the run succeeded.
Getting the result. When the run succeeds, its runner pushes the branch to the same repository, and Chanto prints how to fetch it, plus a link to open a pull request for GitHub repositories:
git fetch origin chanto/run/<id> && git switch chanto/run/<id>Errors. What the server refuses is shown as it says it, with what to do: a hosted run without a card on file or past the org's spending limit (--placement self-hosted keeps it on your runners), an expired or revoked sign-in (chanto cloud login again), an org you're not in. A run that no runner takes in time fails with "no runner took the run in time"; connect one with chanto runner connect or allow hosted runners.
In CI, set CHANTO_CLOUD_URL and CHANTO_CLOUD_TOKEN (an account token, such as the one chanto cloud login saves) instead of logging in, and CHANTO_CLOUD_ORG to pick the org.
Staying signed in. The dashboard's sign-in lasts a year and is pushed forward on every visit, so it only asks again after a long absence or once the token is revoked. The token lives in an HttpOnly cookie that page scripts can't read. Devices in the dashboard lists every browser and CLI sign-in on your account with when it signed in and when it was last used, and revoking one signs that device out; logging out of the browser revokes its token on the server too.
Scheduled and triggered jobs
chanto runner starts agent tasks on a schedule, from a webhook, or on demand. Each one becomes an ordinary background run, with the same worktree isolation, [background] budgets and permission mode, verified mode, needs-input prompts, and notifications as chanto run --bg. Runs started by a job are marked with it ([nightly] … in chanto runs and "job nightly" in notification titles).
Jobs live in .chanto/runner.toml in a project, or in ~/.chanto/runner.toml for jobs of your own:
[[job]]
name = "nightly-review" # letters, digits, - _ .; unique across all job files
schedule = "0 3 * * *" # 5-field cron (local time), or an interval: 30m, 2h, 1d
prompt = "Review yesterday's commits and write up the risks in REVIEW.md."
verify = true # the rest default to [background]
max_minutes = 30
max_cost_usd = 1.0
# mode = "acceptEdits" model = "…" provider = "…" base_url = "…"
# cwd = "services/api" # project jobs: relative to the project (default: its root)
# enabled = false # pause the schedule; triggers still work
# kind = "goal" # work on the prompt as a goal (see Goals)
[[job]]
name = "triage" # no schedule: runs only when triggered
prompt = "Triage the newest failing CI run and propose a fix."A job with kind = "goal" runs its prompt as a goal: the first run waits as needs-input until you approve its criteria, and chanto runs and notifications label it as both the job and a goal run.
Jobs in ~/.chanto/runner.toml need cwd, the absolute path of the checkout to run in. A project's jobs are scheduled once you run chanto runner enable in it; the project must be trusted, because its jobs run unattended. A project job overrides a global one with the same name. Unknown keys are errors, so a typo never silently drops a setting.
When jobs run. The scheduler lives in the Chanto daemon (the process that hosts background runs) and rereads the job files every 30 seconds, so edits apply without a restart. chanto runner enable starts the daemon; chanto daemon start does too. An interval job runs as soon as it is first seen and then every interval after it last started; a cron job waits for its next slot. A slot missed while the daemon was down runs once when it comes back. A job never overlaps itself: while its last run is still going (including waiting for input), it is skipped.
chanto runner # jobs: schedule, next run, last run and its state
chanto runner status # plus whether the daemon is up, enabled projects, problems
chanto runner enable | disable # schedule (or stop scheduling) this project's jobs
chanto runner trigger triage # start a job now (--wait to wait for its result)
chanto runner logs nightly-review # the job's runs, and the latest in detailIn the TUI, /schedule lists the jobs and /schedule run <job> starts one.
From cron, launchd, systemd or CI. chanto runner --once asks the daemon (starting it if needed) to start whatever is due, including the current project's jobs, waits for those runs, prints how each ended, and exits non-zero if one failed (a run that needs input is reported and not waited for). --job <name> starts that job regardless of its schedule, --dry-run only reports, and --no-wait returns once the runs have started. To keep schedules running across reboots, start the daemon at login, for example with a crontab line @reboot chanto daemon start, a launchd agent, or a systemd user unit running chanto daemon start.
Webhooks. POST /runs/jobs/<job>/trigger starts a job. It accepts the daemon's bearer token, or a per-job token from chanto runner token <job> (--rotate replaces it), so a CI system, a Slack workflow or GitHub can trigger exactly that job and nothing else:
curl -X POST -H "Authorization: Bearer $JOB_TOKEN" "$CHANTO_URL/runs/jobs/triage/trigger" # chanto runner token prints bothThe job token is also accepted as an X-Chanto-Token header or a ?token= query parameter, or as a GitHub webhook secret: GitHub signs each delivery (X-Hub-Signature-256) and Chanto checks the signature. A GitHub ping is acknowledged without starting a run. The reply is the run (201), or 409 while the job's last run is still going. Tokens are kept in ~/.chanto/runner/tokens.json (owner-only). The local daemon listens on loopback only; for callers on other machines, put it behind a tunnel or reverse proxy, or use a run server (chanto serve --runs), whose own ~/.chanto/runner.toml jobs it schedules.
Behind the bearer token, GET /runs/jobs lists the jobs with their next and last runs, and POST /runs/jobs/due {job?, project?, dry_run?} is what --once calls. Scheduler state (when each job last ran, and which run) is in ~/.chanto/runner/state.json; the enabled projects are in ~/.chanto/runner/projects.json.