# Scheduled agents
> Run codex, claude or opencode on a cron schedule in a sandboxed container, or on the host with your own login, with only the secrets each agent lists.
Source: https://ivar.run/docs/guide/scheduled-agents

An agent is one Markdown file in `agents/` at the hall root. Its frontmatter
says when and how it runs; its body is the prompt. systemd fires each run;
ivar runs no process of its own between runs. Linux with systemd only.

## An agent file

`agents/triage.md`:

```md
---
schedule: "0 7 * * 1-5"
tool: claude
model: sonnet
network: [api.anthropic.com]
secrets: [ANTHROPIC_API_KEY]
notify: https://hooks.example.com/ivar
timeout: 20m
workdir: app
---
Read the open issues in app and write a triage summary.
```

Every field, its default and every command flag are in the
[`ivar agent` reference](/docs/reference/agent).

## What a run can reach

The container runs as uid 1000 with every capability dropped, no Docker
socket, no home directory and no git credentials. Its only network is an
internal Docker network whose one exit is a tinyproxy that allows the
domains in `network`. To reach a gateway on the host, list
`host.docker.internal`.

## Runtime: host

`runtime: host` runs the `codex`, `claude` or `opencode` already installed
on your machine, with the login you use interactively, so no API key goes
in `secrets`. Each host agent gets a private copy of that login: before
every run, only the tool's login files are copied into
`.ivar/agents/state/<agent>/<tool>/`, and the tool keeps all its state
there. Your own tool config is not writable from the run, so an agent
cannot plant settings, hooks or plugins that run later outside the
sandbox. When the tool refreshes its token during a run, the refreshed
login is written back to your copy, and nothing else is.

Landlock limits its writes to a per-run scratch directory, the agent's
private state directory and, with `access: feature`, the `agent-<name>`
worktree. Its environment starts empty: `PATH`, `HOME`, locale, the
variables that point the tool at its private state, the secrets it lists,
and git pointed at an empty config with no credential helper. No
credential variable is passed. When the run ends, its whole process group
is stopped.

`write` adds paths to that set. Each is resolved and canonicalized when the
run starts. A path that does not exist fails the run with
`agent.write_missing`. Landlock grants a whole subtree, so a path is refused
with `agent.write_refused` when it is, contains or sits inside a hall file
the session write guard protects (`.env`, `.mcp.json`, provider hook configs
such as `.claude/settings.json`), the hall's `.ivar/`, `.git/` or `agents/`,
or the tool's login directory, and when it contains the home, the hall or
the tool's login file. Every `write` is refused when the tool's login
directory holds the home or the hall (`CLAUDE_CONFIG_DIR=$HOME`): point it at
a dedicated directory. A protected path that does not resolve (a symlink
loop, or a dangling link through `..`) also refuses every `write` and is
named in the error. An empty path, `..` or `~user` is refused when the agent
file loads. Anything else you list becomes writable by the agent, including
config other programs read: keep the list to the directories the run must
produce.

`env` adds literal variables after ivar's own and before the secrets, so it
can replace `TMPDIR` or a kept variable such as `LANG`, but never one that
confines the tool. Use it for what a desktop program needs from your
session. Chrome decrypts saved logins only when it reaches your keyring,
which on KDE takes `XDG_CURRENT_DESKTOP`, `DESKTOP_SESSION`,
`KDE_SESSION_VERSION`, `DBUS_SESSION_BUS_ADDRESS` and `XDG_RUNTIME_DIR`.
The D-Bus address hands the run your keyring and every other session
service, so list it only for an agent you trust with them.
`SSH_AUTH_SOCK` likewise hands the run your ssh-agent, so it could push
over SSH.

Chrome's own sandbox writes `/proc/self/uid_map`, so a host agent that
drives Chrome needs `/proc` in `write`. Landlock fixes each rule's path when
the run starts, and Chrome writes from child processes, so nothing narrower
works. `ivar agent sync` warns `agent.host_write_proc` for such an agent: it
can also change the `/proc` entries of your other processes that the kernel
lets your user write, while their memory stays behind ptrace rules.

What it does not isolate: **reads are ambient** and **the network is
open**. Anything your user can read is exposed, including SSH keys and
`~/.config/gh`, and so is any socket in `/run/user/<uid>` it can reach.
With the open network, an agent that can read an SSH key could push
with it. `network` is refused because nothing would enforce it. `ivar agent sync` prints
`agent.host_ambient_read` for every host agent. Use `runtime: docker`
for an agent you would not trust with your home directory.

## Outcomes

`succeeded`, `failed`, `blocked` (the tool asked for input; the run is
stopped at once), `skipped` (the previous run was still going),
`timed_out`. Receipts live in `.ivar/agents/runs/<agent>/<run-id>.json`.

## `access: feature`

The run writes into feature `agent-<name>`. ivar commits on the host after
the container exits and records a `deliver` preview fingerprint in the
receipt. Nothing is pushed: run `ivar feature deliver agent-<name>
--fingerprint <fp>` yourself to open the PR. The apply first needs a
`memory_review` of the feature, or `--skip-memory-review "<reason>"`; see
[Hall memory](/docs/guide/memory).

## Secrets

`.ivar/secrets/agents.env` holds `NAME=value` lines and must be mode 600.
Only the names an agent lists are passed in, and their values are removed
from receipts, logs and notifications.

## Requirements

`ivar doctor` reports each missing piece and its fix: a systemd user
instance, linger (`loginctl enable-linger $USER`), the Docker client, a
reachable daemon, and a warning when Docker runs as root.

For `runtime: host` agents it checks instead that the tool is on `PATH`
(`agent.tool_missing`) and that the kernel has Landlock
(`agent.landlock_unavailable`); without Landlock a host run is refused
and its receipt is `failed`. Docker is not checked when no agent uses it.

Each service pins the `PATH` that `ivar agent sync` ran with, because a
`catchup` run fires as soon as the systemd user manager starts, before your
login session has exported its own `PATH`. That early, the network may be
down too, so a timer run of `claude` or `codex` first waits up to two minutes
for the tool's API host to resolve; after that it starts anyway and records
whatever the tool reports. Run `ivar agent sync` again after installing the
tool somewhere new. Run it with the installed `ivar` too: the service runs the
binary that synced it, and one under a cargo `target/` directory warns
`agent.ivar_bin_in_build_dir`, since a rebuild or `cargo clean` changes or
removes it.

See the [`ivar agent` reference](/docs/reference/agent) for `run`, `logs` and `feedback` flags.
