Persistent flow execution¶
Editions: OSS, Cloud, Enterprise. Unless stated otherwise, everything on this page ships in OSS.
Use Persistent (Govern persistent agent node) when the work should run on an already-enrolled managed agent that is connected to Agent Control, instead of provisioning a short-lived container.
Typical reasons to pick it:
- The agent already has the repository, tools, or session context on its host.
- You want the run governed through the same Agent Control audit trail as console and mobile operator messages.
- An ephemeral clone would be slower or would not see local state.
Fail-fast rule¶
Start does not fall back to an ephemeral container. The execution fails immediately when:
- the flow has no
target_agent_id - the target is missing, inactive, or not an Agent Control kind
- the target only accepts text on an already-open session (currently Pi and DeepSeek); persistent start always opens a new session
- Agent Control is not verified on that agent
- the agent's control heartbeat is stale (it is offline)
The flow form shows each target's Agent Control state and disables agents that are not online. If you save an offline target, start still fails until that agent reconnects. Connected Pi and DeepSeek targets can still appear in the picker; start refuses them because they cannot open a new session.
What happens at run time¶
- The orchestrator renders the flow prompt the same way as the ephemeral path.
- Preloop persists one
send_messagecommand, then delivers it to the target. - The execution stays
RUNNINGwhile the command is pending, delivered, or acked without a result. - A successful
command_resultmarks the execution succeeded. Acommand_error, expiry, or stop marks it failed. - If the flow timeout budget expires, Preloop interrupts the agent's
current session (
session_mode: current) and stops waiting. That is usually this flow's turn; it is not a guarantee if another session started.
See Flow execution on a persistent agent for the envelope, binding, and status table.
Checkout and clone-less¶
Persistent execution sends a workspace object on the send_message.
- Checkout (
workspace.modeispersistent_checkout): the flow has git clone enabled and the trigger names a repository. The sidecar keeps<workspace_root>/<repository_slug>on the agent host, clones it once with the host's own git credentials, then fetches and checks out later runs. No tracker token is sent to the sidecar. - Clone-less (
workspace.modeisclone_less): git clone is disabled, no repository could be resolved, or the flow lists more than one repository. The preset must not run git. The pull request reviewer reads the diff from the tracker and says so in the review. - Ephemeral runs are unchanged. Their prompt renders
workspace.modeasephemeraland the container still clones into its own workspace.
Presets declare supports_persistent. The marker means the prompt was
checked against persistent modes. It does not mean the host captures
container result files. When Persistent is selected, the flow form
disables presets that do not support it. The pull request reviewer
supports it. Presets that write a container result path, or that still
assume an ephemeral checkout, do not.
Run in a directory on the agent host¶
On a Claude Code target, a flow can name the directory the session runs in
with agent_config.workspace. Other targets (for example Codex) do not
support it yet, and start fails instead of running in their default
directory.
{
"execution_path": "persistent",
"target_agent_id": "<managed agent id>",
"workspace": {"kind": "authorized_directory", "id": "dir_..."}
}
The host owner authorizes the directory first:
preloop runner dirs add ~/code/project --mode write --harness claude_code
preloop runner dirs list # shows the dir_... id
{"kind": "path", "path": "/abs/path"} is also accepted, but only when that
path is an authorized directory on the host. Preloop validates the shape when
the flow is saved; the Claude sidecar resolves it at run time against
~/.preloop/runner.json and fails the execution when:
- the id or path is not in the authorized list, or the entry is
read_onlyor limited to other harnesses - the directory is the filesystem root, the home directory (or contains it),
or a credential directory such as
~/.ssh,~/.claudeor~/.preloop - the directory has uncommitted changes, unless the flow sets
"allow_dirty": true
A named directory replaces the checkout for that run: git clone settings are
not used. Without workspace, the session runs in workspace_root from the
sidecar config, as before. The sidecar ignores a raw cwd in the message, so
a message cannot point a session at a directory the host owner did not list.
Model¶
When the flow's model is a Claude model (Anthropic provider, an identifier
containing claude, or a row named Claude Code <alias>), Preloop sends its
name as metadata.model: the gateway alias for gateway-routed models, else
the alias embedded in that display name, else the model identifier. The Claude
sidecar starts the session with that model (options.model / --model).
Other models are not sent, and the host's own Claude Code setting applies.
Turn timeout¶
The sidecar waits for one turn as long as the flow timeout
(run_limits.max_duration_seconds, else timeout_seconds), up to 24 hours.
turn_timeout_ms in ~/.claude/preloop-control.json, when set, is an upper
bound. With neither, the default stays 5 minutes.
Unattended scheduled runs¶
A scheduled persistent run has nobody at the terminal. Every tool call still
goes through the Preloop PreToolUse hook installed by
preloop agents onboard "Claude Code" --approvals, so a call that escalates
to a human waits for an approver and fails when the approval expires.
Give the flow an approval policy so the calls it needs are approved for that run only:
{
"execution_path": "persistent",
"target_agent_id": "<managed agent id>",
"workspace": {"kind": "authorized_directory", "id": "dir_..."},
"approval_policy": {
"mode": "auto_allow",
"tools": ["Bash", "Read", "Edit", "Write", "Glob", "Grep"],
"paths": ["<workspace>"],
"commands": ["git status", "git diff", "git add", "git commit", "pytest"]
}
}
modeisask(the default: nothing changes) orauto_allow.toolsmay listBash,Read,Edit,MultiEdit,Write,Glob,Grep,LS,NotebookEditandTodoWrite. Network tools and MCP tools are not eligible.pathsare absolute directories on the agent host.<workspace>is the flow'sworkspace.pathwhen it names one, else the directory the session runs in.commandsis required whentoolslistsBash: the command prefixes (whole words) a Bash call may start with. Shells, interpreters and command runners (sh,bash,python,node,env,xargs,eval,find,awkand similar) cannot be listed, and tools such asgit,npmordockermust be listed with their subcommand (git commit, notgit). git calls with options that run other programs or move its config (-c,-C,--exec-path,--upload-pack,--git-dirand similar) are not covered.
How the grant is scoped:
- The sidecar starts the flow's Claude Code session with
PRELOOP_FLOW_EXECUTION_ID, and the hook sends it with each permission check. The grant only exists while that execution is active, only for the managed agent the execution was dispatched to, and only for the first native session that presents the execution id. Interactive sessions are not affected. - A covered call is still recorded as an approval request, resolved as auto-approved by the flow's approval policy, so the audit trail shows it.
- Not covered, so it takes the normal approval path: file paths outside
paths; a Bash call whose working directory is outsidepaths; a Bash command with any word that does not start with a listed prefix; a command with$, backticks, backslashes, newlines, redirections, subshells or background jobs; a word that names a path outsidepaths, or uses~or..;git push,fetch,pull,clone,ls-remote,submodule, and everygit remotesubcommand exceptshowandget-url; network tools such ascurl,wget,sshandgh, publishing commands, andsudo, even when quoted. Deny rules and require-approval rules in your approval policies still win.
What the checks do not cover, so scope the policy accordingly:
- The checks read the command and paths as written. The control plane
cannot resolve symlinks on the host, so a symlink inside
pathsthat points elsewhere is not detected. - A listed command can run code from the workspace:
pytestruns test files andconftest.py,npm testruns package scripts, and the session can edit those files. Auto-approving such a command gives the session the host user's rights for whatever that code does. List only what the run needs.
Also recommended:
- Set
"permission_mode": "acceptEdits"in the sidecar control config, so file edits do not wait on Claude Code's own prompt, which nobody can answer in an owned session. Do not usebypassPermissions. - Point the flow at an authorized directory (above) and keep the tree clean,
or set
allow_dirtydeliberately. - Set the flow timeout to cover the whole run.
What this does not do yet¶
Persistent mode does not forward git credentials. The Claude sidecar and the Codex sidecar both implement this workspace checkout contract. Codex is already on the Agent Control allow-list.