Configuration Reference
Animus resolves behavior from project YAML, installed pack layers, scoped runtime state, and environment overrides.
Project-Local Sources
.animus/config.json
Repository-local Animus configuration created during setup.
The optional auto_update block configures self-update behavior for the animus CLI binary (see Self-update below). Omitted by default; absence behaves the same as {"mode": "notify", "check_interval": "P1D", "channel": "stable"}.
.animus/workflows.yaml and .animus/workflows/*.yaml
These YAML files are the editable workflow source of truth for a project.
Typical uses:
- define repo-specific workflow ids such as
standard-workflow - define the repository's default workflow explicitly
- declare project MCP servers, agents, variables, phases, and workflow definitions
Workflow YAML capabilities
Workflow YAML is more than per-workflow phase plumbing — three top-level blocks extend the daemon itself:
schedules— cron-driven workflow dispatch (UTC; forward-only, missed runs are not replayed). See Workflow YAML: schedules.triggers— event-driven dispatch from file watchers, generic webhooks, GitHub webhooks, or trigger backend plugins. See Workflow YAML: triggers.daemon— workflow-YAML-side daemon settings:active_hours,phase_routing,mcp, andbudget. Thepool_size,interval_secs,max_task_retries, andretry_cooldown_secskeys were removed fromDaemonConfig: declaring them in workflow YAML emits a removed-key warning.pool_sizeandinterval_secsare configured via the persisted daemon project config or CLI flags (animus daemon config/daemon run/daemon start);max_task_retriesandretry_cooldown_secshad no runtime sink and were dropped entirely. The daemon git/merge policy keys (auto_merge,auto_pr,auto_commit_before_merge,auto_prune_worktrees) were removed in v0.5.x, and workflowpost_success.mergewas removed too. Git/PR actions now belong in explicit workflow command phases or external automation. See Workflow YAML: daemon.
All three are optional. They live at the top level of any .animus/workflows.yaml or .animus/workflows/*.yaml file. schedules: and triggers: entries merge across files by id (later overlays override earlier entries). The daemon: block does not field-merge — any overlay that defines daemon: replaces the previously-accumulated block wholesale, so keep daemon: in a single file.
Hot-reload
When the daemon is running, edits to .animus/workflows.yaml and any file under .animus/workflows/*.yaml are picked up automatically by a filesystem watcher (the notify crate's recommended backend per platform). The daemon also re-reads the runtime-reconfigurable settings in ~/.animus/<repo-scope>/daemon/pm-config.json once per scheduler tick (see Daemon scheduler timing), so animus daemon config changes apply without a restart. .animus/plugins.lock and daemon transport settings still require a daemon restart.
Behaviour:
- A 500 ms debounce coalesces editor write-bursts (vim's
.swp+rename, vscode's atomic temp-then-rename) into a single reload. - On success: the most-recently compiled
WorkflowConfigis stored in anarc-swapsnapshot and the daemon broadcasts aconfig_reloadedevent on theworkflow/eventscontrol subscription. Subscribers see{workflow_id: "<daemon>", kind: "config_reloaded", payload: {...}}. - On failure: the daemon logs the YAML diagnostic, broadcasts a
config_reload_failedevent, and keeps the prior snapshot active. A malformed edit never crashes the daemon. Note that daemon paths today still read directly from disk viaload_workflow_config_with_metadatafor each workflow run, so a malformed-then-saved overlay can still surface a parse error to the next workflow that starts; the snapshot guarantees the hot-reload broadcast contract and the carry-over for callers that consume the snapshot directly. Routing the rest of the daemon's workflow paths through the snapshot is tracked separately. - Daemon transport settings (control socket, MCP, plugin process layout) are not affected by a reload — those still require a daemon restart.
macOS note: notify's recommended backend on macOS is FSEvents, which delivers file-system events that are already coarsened by the kernel. A single editor write often surfaces as a single batched event regardless of the userland write pattern, so the 500 ms debounce window is largely redundant there but kept for parity with Linux's finer-grained inotify and Windows' ReadDirectoryChangesW.
Manual trigger (useful when the host filesystem's notify backend is flaky):
animus workflow config reload [--json]JSON envelope shape (matches the animus.cli.v1 wrapper):
- success:
{"reloaded": true, "phase_definitions": N, "workflows": M, "agent_profiles": K, "source_files": [...], "config_hash": "<sha256 of compiled workflow config>"} - failure:
{"reloaded": false, "errors": [{"message": "<diagnostic>"}], "source_files": [...]}
.animus/plugins/<pack-id>/
Project-local pack overrides. Use this when a repository needs to override installed pack content without changing Animus globally.
The parent .animus/plugins/ directory is also scanned as the project-local plugin discovery directory, so the same tree can contain both plugin binaries and pack override folders.
.animus/plugins.lock
Project-local plugin integrity lockfile. When plugin install/update flows are scoped to the repository, Animus records installed plugin versions and sha256 digests here instead of falling back to the global ~/.animus/plugins.lock.
The lock is the SOURCE OF TRUTH for the installed plugin set. Every mutating op (plugin install / update / uninstall / install-defaults, and install --locked) writes the lock as the authoritative record and then REGENERATES plugins.yaml (the registry) deterministically FROM the lock. The registry is therefore a DERIVED cache — a materialized projection of the lock that exists only so hot-path discovery can read plugin paths without re-deriving them. The two can never drift: a registry row whose name has no lock entry is dropped on the next regeneration, and a lock entry missing from the registry is added. plugin lock verify reports any drift (installed but unlocked, or pinned but missing on disk) so the next mutating op heals it; a project with an existing plugins.yaml but no lock (pre-v0.6.3) materializes a lock from the installed set on the next mutating op without losing any plugin (discovery keeps working off the registry until then).
Each [[plugins]] entry records:
name,version,artifact_sha256,signature_bundle_sha256,installed_at— integrity audit trail.installed_kind(v0.5.7+) — user-facing kind the SubjectRouter dispatches against (e.g.task,task-2,archive). Set by the install pipeline; auto-incremented on collision unless the operator passesanimus plugin install --as-kind <KIND>.native_kind(v0.5.7+) — plugin-native kind declared in the manifest (e.g.task). The SubjectRouter translates<installed_kind>/<verb>→<native_kind>/<verb>at the wire boundary.
Entries written by pre-v0.5.7 installers parse cleanly; consumers treat the missing fields as installed_kind == native_kind. See Plugin kind translator (v0.5.7).
Project vs global plugin installs
animus plugin install supports two install scopes; each scope owns a matching (install dir, registry, lockfile) triple:
| Global (default) | Project (--project) | |
|---|---|---|
| Binaries | ~/.animus/plugins/ (or $ANIMUS_PLUGIN_DIR / --plugin-dir) | <project>/.animus/plugins/ |
| Registry | ~/.animus/plugins.yaml | <project>/.animus/plugins.yaml |
| Lockfile | ~/.animus/plugins.lock (project lockfile preferred when .animus/ exists) | <project>/.animus/plugins.lock |
--project is mutually exclusive with --plugin-dir, and the same flag exists on animus plugin uninstall and animus plugin update. Project installs run the identical integrity pipeline as global ones: sha256 verification, cosign signature policy, publisher TOFU, and lockfile fail-closed checks.
Shadowing rule. Discovery scans the project-local <project>/.animus/plugins/ tier FIRST, so on a name collision the project-local install wins over both a registry-recorded global install and a bare binary in the global install dir. animus plugin list marks each row with its scope (project / global) and surfaces hidden global binaries in a shadowed array. The per-project plugin scope file (.animus/plugin-scope.yaml) filters project-installed plugins exactly as it does global ones — an allowlist that omits a project-installed plugin excludes it from discovery.
Active flavor (active_flavor:). .animus/plugin-scope.yaml also carries an optional active_flavor: key recording which flavor the project opted into. It is written by a successful animus plugin install-defaults --flavor <name> / animus flavor install <name> (a default selection is omitted, keeping the common-case file clean). The daemon's flavor-only scope resolver and animus plugin list / animus plugin scope show read it back, so a non-default flavor's plugins are admitted by flavor-only scoped discovery instead of always being matched against flavors/default.toml. When the recorded name has no flavors/<name>.toml on disk (a stale selection — the manifest was renamed or removed), the resolver logs a warning and falls back to the default flavor's plugin set (resolved from flavors/default.toml or the binary-bundled default) for the admit set — it never fail-closes discovery to an empty admit set. The scope file's mode is preserved (a flavor-only file stays flavor-only, now scoping against the default flavor's plugins). animus plugin scope show reports the raw persisted active_flavor name plus its active_flavor_source (persisted | default) for diagnostics, even when the resolver fell back to the default flavor's plugins. Selecting default via install-defaults/flavor install clears the persisted key and re-synthesizes a working scope mode (downgrading a leftover flavor-only to all when no on-disk flavors/default.toml exists).
Version control. Project plugin BINARIES should not be committed — animus init and animus plugin install --project write a .animus/.gitignore covering plugins/. The project lockfile (.animus/plugins.lock) SHOULD be committed: that is how a repository pins its own plugin set, and animus plugin lock verify (which sweeps both the global and project lockfile roots by default) turns the committed lock into a CI tamper/drift gate. The project registry (.animus/plugins.yaml) is also safe to commit; it carries install provenance, not secrets. Note the registry is a DERIVED projection of the lock (regenerated on every mutating op — see .animus/plugins.lock above), so committing the lock is what actually pins the set; a committed registry that drifts from the lock is reconciled to the lock on the next install/update/uninstall.
Repo-Scoped Runtime Config
Animus stores mutable project runtime config under ~/.animus/<repo-scope>/.
Key files:
config/state-machines.v1.jsonconfig/workflow-config.v2.jsonconfig/agent-runtime-config.v2.jsonstate/pack-selection.v1.jsondaemon/pm-config.jsonresume-config.json
These files are Animus-managed state. Treat them as runtime data, not hand-authored config.
daemon/pm-config.json
Persisted daemon runtime settings, written by animus daemon config (CLI) or animus.daemon.config-set (MCP) — never project-local .animus/. The runtime-reconfigurable keys are pool_size, interval_secs, max_tasks_per_tick, stale_threshold_hours, phase_timeout_secs, and silent_threshold_mins; the running daemon re-reads them once per scheduler tick, so changes apply without a restart. Exception: the daemon's ProcessManager captures phase_timeout_secs once at startup, so a changed phase timeout only takes effect for the live process manager after a daemon restart (the per-tick reload updates the scheduler options only). Unknown keys (including fields removed in earlier releases, such as the v0.5.x daemon git/merge policy keys and the no-op idle_timeout_secs) round-trip untouched so old files keep loading.
Daemon scheduler timing
The daemon's scheduling model has two layers:
- Ticks are interval-driven. The scheduler loop sleeps
interval_secsbetween ticks (default 5, clamped to a minimum of 1 second). Each tick re-readsdaemon/pm-config.json, runs housekeeping, evaluates schedules and triggers, and dispatches ready work. - Phases within a running workflow are reactive. Once a workflow is dispatched, phase completions are consumed as they arrive; the next phase does not wait for the next tick.
Cron schedules (schedules: in workflow YAML) fire on the first tick at or after their occurrence, so punctuality is bounded by interval_secs. A catch-up horizon (10 minutes) absorbs long ticks or interval_secs well above 60 with at most one catch-up fire; occurrences missed for longer (daemon stopped, active_hours gate closed) are skipped, not replayed.
Two distinct knobs bound dispatch:
pool_size— hard cap on concurrently active agents/workflow runners. The effective cap ismin(pool_size, ANIMUS_WORKFLOW_CONCURRENCY_MAX)(quota default 10); whenpool_sizeis unset, the quota alone applies.max_tasks_per_tick— how many new workflows one tick may dispatch (default 2). Per tick the dispatch budget ismin(max_tasks_per_tick, pool capacity - active agents), and schedules and triggers draw from that same shared budget.
So pool_size limits steady-state concurrency while max_tasks_per_tick limits ramp-up rate: with pool_size 8, max_tasks_per_tick 2, and an empty pool, reaching 8 concurrent workflows takes at least 4 ticks.
Global User Config
~/.animus/config.json
The global Animus config stores machine-local user settings such as:
- agent runner auth token
- user-defined MCP server entries
- Claude profile launch environments
Use ANIMUS_CONFIG_DIR to override the global config root in tests or custom environments.
Example:
{
"claude_profiles": {
"main": {
"env": {
"CLAUDE_CONFIG_DIR": "/Users/alice/.claude-main"
}
}
}
}Installed Sources
Machine-installed packs
Installed packs live at:
~/.animus/packs/<pack-id>/<version>/Manage them with:
animus pack list
animus pack info --pack-id animus.task
animus pack install --path /tmp/vendor.pack --activate
animus pack pin --pack-id vendor.pack --version =1.2.3Configuration Precedence
Behavior resolves in this order:
- CLI flags
- supported environment variables
- project-local pack overrides in
.animus/plugins/<pack-id>/ - project YAML in
.animus/workflows.yamland.animus/workflows/*.yaml - installed packs in
~/.animus/packs/
Secrets vs. Non-Secret Config
Plugin-owned credentials still stay out of YAML. As of v0.5.8, plugins can read required env vars from either the daemon's process environment or the project-scoped keychain-backed secret store. Resolution order is: explicit parent-process env first, then the keychain entry for the current
repo-scope.For credentials that a phase or MCP server declared in YAML needs to receive (e.g. an MCP server's
env:block), v0.5.5 adds thesecrets:block — declare the mapping once, reference with${secret.<name>}, and the compiler resolves it at config-compile time using the same env-first, keychain-second lookup chain.Use
${VAR}interpolation for non-secret configuration that varies by environment: API base URLs, team IDs, feature flags, channel allowlists.
Where credentials live
| Plugin | Env var it reads | Set on |
|---|---|---|
animus-subject-linear | LINEAR_API_TOKEN | Parent-process env, else animus secret keychain entry |
animus-provider-oai | OPENAI_API_KEY (and provider-specific overrides) | Parent-process env, else animus secret keychain entry |
animus-provider-claude | inherits the Claude CLI's existing auth | The daemon's process environment |
animus-provider-codex | inherits the Codex CLI's existing auth | The daemon's process environment |
| future plugins | declared in the plugin's plugin.toml [[env]] section or its README | Parent-process env, else animus secret keychain entry when the plugin declares that variable |
Seed credentials either by exporting them before daemon start or by storing them with animus secret set <KEY>. Explicit process env still wins on collision. Example:
LINEAR_API_TOKEN=lin_api_... OPENAI_API_KEY=sk-... animus daemon startanimus secret set LINEAR_API_TOKEN --value lin_api_...
animus secret set OPENAI_API_KEY --value sk-...
animus daemon startWorkflow YAML interpolation (non-secret config)
Workflow YAML supports shell-style ${VAR} interpolation for non-secret config only so the same YAML can target dev, staging, and prod without edits. Substitution is performed by the workflow YAML loader before the YAML is parsed, so every string scalar in the file (.animus/workflows.yaml, .animus/workflows/*.yaml, and pack-shipped workflow overlays) accepts the same syntax uniformly. Lookup order is std::env first, then the project-scoped keychain-backed secret store installed by animus secret.
Syntax
| Form | Meaning |
|---|---|
${VAR} | Required. Errors with file path + line number if VAR is unset. |
${VAR:-default} | Optional. Falls back to the literal default when VAR is unset or empty. |
${VAR:?message} | Required with a custom error message. |
$$ | Literal $. |
A lone $ not followed by { or $ passes through unchanged, so prose like cost $5 is preserved.
Env var names follow POSIX shell rules — they must start with a letter or underscore and contain only letters, digits, and underscores. The loader rejects invalid names with the offending file path and line number.
Example
# CORRECT — non-secret config in YAML, secrets from plugin env
subjects:
- id: my-linear
backend: linear
config:
team_id: ${LINEAR_TEAM_ID:-default-team-uuid}
api_url: ${LINEAR_API_URL:-https://api.linear.app/graphql}
workspace: ${LINEAR_WORKSPACE:?set LINEAR_WORKSPACE for this workflow}
# LINEAR_API_TOKEN is set in the daemon's environment, NOT here.
# animus-subject-linear reads it directly from its process env at startup.
# Run the daemon with: LINEAR_API_TOKEN=lin_api_... animus daemon start# WRONG — DO NOT DO THIS
subjects:
- id: my-linear
backend: linear
config:
api_token: ${LINEAR_API_TOKEN} # secrets should not be in YAML at allError reporting
When a required ${VAR} is unset, the loader fails fast with a message that cites the YAML file path and 1-based line number:
workflow YAML at .animus/workflows/agents.yaml line 12 references unset env var LINEAR_TEAM_ID..env files
The daemon does not auto-load .env files. To pre-load variables before starting the daemon, source them in the parent shell:
set -a; source .env; set +a
animus daemon startOr use a process supervisor (systemd, launchd, docker-compose) that supports an EnvironmentFile directive.
For persistent machine-local storage without keeping plaintext in the shell environment, use animus secret import-env to migrate selected .env entries into the OS keychain, then remove the plaintext file.
What gets interpolated
Every string scalar in the YAML — including subject configs, provider tokens, workflow metadata, env override blocks, and any future plugin-config field — is substituted. Numeric and boolean YAML values pass through unchanged. Comments are not stripped before substitution, so # ${VAR} inside a comment is also substituted; this is intentional and matches docker-compose semantics.
External prompt files
Long agent system prompts can live in their own files outside .animus/workflows.yaml. Each AgentProfile accepts an optional system_prompt_file that points at a UTF-8 text file:
- Relative paths resolve against the source YAML file's parent directory; absolute paths are taken as-is.
- The file is read at config-compile time and inlined verbatim into the agent's
system_prompt(no trimming or normalization). The compiledworkflow-config.v2.jsonis self-contained —system_prompt_fileis dropped after resolution. system_prompt(inline) andsystem_prompt_fileare mutually exclusive on the same agent. Setting both fails with an error naming the agent id, the source YAML file, and (when detectable) the line number.- Missing files, non-UTF-8 contents, and files larger than 1 MiB cause compile to fail with the resolved absolute path in the error.
agents:
implementer:
description: "Implementer"
system_prompt: "You are the implementer. Keep changes minimal."
researcher:
description: "Researcher"
system_prompt_file: prompts/researcher.mdHTTP MCP servers
The top-level mcp_servers: map in workflow YAML declares MCP servers that agents can attach to. Each entry must pick exactly one transport:
transport: httprequiresurl:(anhttp://orhttps://endpoint).command,args, andenvmust not be set on http entries.transport: stdio(the default when omitted) requirescommand:; optionalargs:andenv:are supported.urlmust not be set on stdio entries.
Agents reference servers by name via agents.<id>.mcp_servers: [name1, name2]. The name must match a key in the top-level mcp_servers: map; an unresolved name fails config validation with a clear error.
Env-var interpolation applies to the entire workflow YAML before parsing, so it reaches url: and command: fields too — useful for swapping endpoints per environment:
mcp_servers:
robinhood-trading:
transport: http
url: ${ROBINHOOD_MCP_URL:-https://agent.robinhood.com/mcp/trading}
animus:
transport: stdio
command: animus
args: ["mcp", "serve"]
agents:
trader:
description: "Trade execution agent"
mcp_servers:
- robinhood-trading
- animusThe compiled workflow-config.v2.json carries the resolved server definitions, and each phase's runtime contract surfaces them under /mcp/additional_servers in the shape the agent runner expects (a url field for http transport, command/args/ env for stdio).
Note: Animus launches Claude with --strict-mcp-config, so user-scope claude mcp add registrations are not visible to Animus-spawned sessions. Servers an agent needs to call must be declared in workflow YAML (or supplied by a pack overlay or project-level mcp_servers config).
HTTP MCP OAuth (v0.5.5)
HTTP-transport MCP servers can attach an oauth: block so the daemon resolves a bearer token and injects an Authorization: Bearer <token> header into the additional MCP server entry passed to the agent. Tokens never appear in YAML — all credential material is read from process environment variables named via *_env pointers in the block. See the workflow-yaml HTTP transport with OAuth section for the full shape and validation rules.
Token cache: ~/.animus/<repo-scope>/mcp-oauth-cache/<server>.json with 0600 permissions on Unix. The daemon reads client_id / client_secret / refresh_token / bearer env vars from the daemon process's own environment, mirroring how plugin secrets are sourced today (see docs/reference/configuration.md#secrets-vs-non-secret-config).
Self-update
The animus CLI can poll its own GitHub releases (launchapp-dev/animus-cli) on startup and surface, prompt for, or silently apply newer builds. The behavior is fully configurable via the auto_update block in .animus/config.json (or the global ~/.animus/config.json):
{
"auto_update": {
"mode": "notify", // off | notify | prompt | auto
"check_interval": "P1D", // ISO-8601 duration; default daily
"channel": "stable" // stable | prerelease
}
}Modes:
off— never check, never notify, never apply.notify(default) — print a one-liner to stderr when a newer release is available, but do nothing else.prompt— ask[y/N]on stderr when a newer release is available. When stdin is not a TTY (CI, piped invocations) this degrades tonotify.auto— download and atomically install the newer release in the background. The currently-running invocation continues to run on the old binary; the swap takes effect on the next invocation.
check_interval accepts an ISO-8601 duration string (P1D, PT6H, P1W, PT30M, ...). The last-check timestamp persists at ~/.animus/auto-update-state.json.
channel filters releases: stable drops anything tagged prerelease: true on GitHub; prerelease admits both. Pass animus update --prerelease to override per-invocation.
Manual trigger: animus update bypasses the mode check and applies the newest matching release. Flags:
--check— print the available version and exit 0 if newer, non-zero otherwise. Useful in CI.--force— re-install the latest tag even when already current (repair).--prerelease— include prereleases regardless ofchannel.--channel <stable|nightly>— pick the release channel to poll.--yes— skip the interactive confirmation prompt.
Integrity: when the GitHub release asset has an inline digest: sha256:<hex> field, the staged download is verified against it before the atomic swap. Releases that ship a sidecar <asset>.sha256 are also honored. Releases that do neither are downloaded but not hash-verified — operators are encouraged to publish digests upstream.
The startup check is fire-and-forget — it runs as a tokio background task and never delays the subcommand the user actually asked for. Network failures during the background check are silently dropped; run animus update --check to surface them.
Environment Variables
The complete v0.4.0 env var surface was renamed from AO_* to ANIMUS_*. There are no legacy aliases; the old names will not be read.
Core
| Variable | Description |
|---|---|
ANIMUS_CONFIG_DIR | Override the global Animus config directory (default ~/.animus) |
ANIMUS_DISABLE_CI_CACHE | Truthy (1, true, yes, on) — disable the per-project CI status cache used by animus status (~/.animus/<repo-scope>/cache/ci-status.json), forcing a fresh gh lookup every call |
ANIMUS_CI_CACHE_TTL_SECS | TTL in seconds for the CI status cache (default 60). Honored on every read, so lowering it immediately invalidates older entries |
ANIMUS_MCP_SCHEMA_DRAFT | Select Draft-07 MCP tool input schemas |
ANIMUS_MCP_ENDPOINT | Override the MCP server endpoint for the CLI's embedded client |
ANIMUS_USER_ID | Override the recorded user id for authored actions |
ANIMUS_ASSIGNEE_USER_ID | Override the assignee user id when creating tasks |
ANIMUS_DEBUG | Enable verbose debug logging across the CLI and daemon |
ANIMUS_LOG_JSON | Emit log lines as JSON for log shippers |
ANIMUS_DEBUG_MCP_STDIO | Log raw MCP stdio frames for plugin/server debugging |
ANIMUS_AUTO_UPDATE_MODE | Override the configured auto_update.mode for the current process (off, notify, prompt, auto) |
ANIMUS_AUTO_UPDATE_DISABLE | Short-circuit auto-update to off regardless of config when set to a truthy value |
ANIMUS_METRICS_DISABLE | Truthy — hard kill switch for opt-in anonymous metrics. Suppresses emission regardless of any persisted opt-in. See Opt-in anonymous metrics. |
Plugins and templates
| Variable | Description |
|---|---|
ANIMUS_PLUGIN_DIR | Override the global plugin install directory used by animus plugin install. Discovery always scans the resolved global install dir: $ANIMUS_PLUGIN_DIR when set, otherwise ~/.animus/plugins/. --plugin-dir <PATH> overrides it for install/uninstall commands |
ANIMUS_PLUGIN_PATH | Colon-separated list of additional directories to scan for animus-provider-* and animus-plugin-* binaries during plugin discovery |
ANIMUS_TEMPLATE_REGISTRY_URL | Override the default template registry URL used by animus init. Defaults to the LaunchApp project-templates registry |
Runner and workflow
| Variable | Description |
|---|---|
ANIMUS_WORKFLOW_RUNNER_BIN | Override the workflow-runner binary path |
ANIMUS_PHASE_RUN_ATTEMPTS | Maximum attempts for a single phase run before giving up |
ANIMUS_PHASE_MAX_CONTINUATIONS | Cap on phase continuation rounds |
ANIMUS_REPLAY_SESSION | Path to a recorded decision log; when set (non-empty), the workflow runner replays provider decisions from that session instead of invoking the live provider |
ANIMUS_RUNNER_SESSION_DIR | Override the runner-sessions sidecar directory used to look up provider session ids for resume (default ~/.animus/runner-sessions/) |
ANIMUS_COST_STATE_ROOT | Override the root directory for cost-state.v1.json and decisions.jsonl. Test seam — production callers leave it unset and the standard ~/.animus/<repo-scope>/ root is used |
Daemon runtime quotas
Per-process caps the daemon resolves once at startup (RuntimeQuotas::from_env). Unset, empty, non-numeric, or 0 values fall back to the default, so an accidental empty override cannot disable a cap.
| Variable | Default | Description |
|---|---|---|
ANIMUS_TRIGGER_BACKLOG_MAX | 1000 | Max unprocessed webhook/trigger events retained per trigger; the oldest are dropped (with a warning) beyond this |
ANIMUS_SUBSCRIBER_MEMORY_MAX_MB | 10 | Approximate per-subscriber buffer cap (MB) for workflow/events subscriptions; a lagging subscriber is terminated with reason buffer_full_lagged |
ANIMUS_PLUGIN_PROCESS_MAX | 50 | Max concurrently-spawned plugin child processes; further spawns are refused with an error |
ANIMUS_WORKFLOW_CONCURRENCY_MAX | 10 | Max workflow runner subprocesses dispatched in parallel; excess requests queue until headroom appears. Also upper-bounds pool_size (see Daemon scheduler timing) |
Fleet daily spend cap
A fleet-level wallet kill-switch: a USD ceiling on the daemon's total rolling-24h spend, distinct from the per-workflow / per-phase budget: caps in workflow YAML. When the rolling spend crosses the cap, the daemon's housekeeping sweep latches a flag and the tick suppresses all new dispatch (cron schedules, triggers, ready tasks, and explicit queue-drain entries) until spend ages out of the rolling window or the operator raises/clears the cap, at which point dispatch resumes automatically on the next sweep. In-flight phases are never interrupted.
Set it via the scoped daemon runtime config (preferred, hot-reloaded):
animus daemon config --max-daily-usd 50 # cap at $50 / rolling 24h
animus daemon config --max-daily-usd 0 # clear the cap (uncapped)The cap is stored as max_daily_usd in ~/.animus/<repo-scope>/daemon/pm-config.json. An explicit pm-config value (positive or zero) always overrides the workflow-YAML daemon.budget.max_cost_usd_per_day fallback. Window semantics: a rolling 24-hour window, not a calendar day (no timezone applies).
Observability: the cap, today's rolling spend, remaining headroom, and the dispatch-paused state appear under budget_enforcement.daily_cap in animus daemon health --json and as the daily_* fields in animus cost summary (--json and text). The latch and a fleet breach record also persist alongside the cost state under the scoped root (daily-cap.v1.json; the breach is logged to decisions.jsonl with run id fleet:daily-cap, visible via animus cost decisions). The ANIMUS_DAEMON_DISABLE_BUDGET_ENFORCEMENT kill-switch disables the fleet cap along with the rest of budget enforcement.
Notifications
| Variable | Description |
|---|---|
ANIMUS_NOTIFY_WEBHOOK_URL | Common env var referenced by generic webhook connectors via url_env: "ANIMUS_NOTIFY_WEBHOOK_URL" |
ANIMUS_NOTIFY_BEARER_TOKEN | Common bearer-token env var referenced from headers_env, for example "headers_env": { "Authorization": "ANIMUS_NOTIFY_BEARER_TOKEN" } |
ANIMUS_NOTIFY_MISSING_URL | Behavior tuning for notification dispatch when the webhook URL is unset |
Any other env var name can be referenced by a notification config's url_env or per-header env lookups, so projects can define their own ANIMUS_NOTIFY_* variables and reference them from the persisted notification config.
Notification config is persisted under the repo-scoped daemon runtime file ~/.animus/<repo-scope>/daemon/pm-config.json, not under project-local .animus/. The daemon forwards only the env var names explicitly referenced by that notification_config block's *_env fields (including object-valued headers_env maps) into the notifier plugin subprocess, so unrelated daemon credentials are not re-exposed to notifier plugins.
Provider plugin tuning
These passthrough variables are read by the installed provider plugins (claude, codex, gemini, opencode) when spawning their underlying CLIs.
| Variable | Description |
|---|---|
ANIMUS_CLAUDE_BYPASS_PERMISSIONS | Pass --dangerously-skip-permissions (or equivalent) to the Claude provider plugin. Intended for unattended test environments only — never set in production |
ANIMUS_CLAUDE_EXTRA_ARGS / _JSON | Extra args to forward to the Claude CLI |
ANIMUS_CODEX_EXTRA_ARGS / _JSON | Extra args to forward to the Codex CLI |
ANIMUS_CODEX_EXTRA_CONFIG_OVERRIDES / _JSON | Extra --config overrides for Codex |
ANIMUS_CODEX_NETWORK_ACCESS | Toggle Codex sandbox network access |
ANIMUS_CODEX_WEB_SEARCH | Toggle Codex web search capability |
ANIMUS_GEMINI_EXTRA_ARGS / _JSON | Extra args to forward to the Gemini CLI |
ANIMUS_OPENCODE_EXTRA_ARGS / _JSON | Extra args to forward to the OpenCode CLI |
ANIMUS_AI_CLI_EXTRA_ARGS / _JSON | Generic CLI passthrough used by the wrapper crate |
External / inherited
| Variable | Description |
|---|---|
CLAUDECODE | Set by the Claude Code harness when an embedded session is active. Animus detects it and unsets it before spawning a nested claude CLI to avoid the "cannot launch inside another Claude Code session" guard |
Plugin kill-switches
These environment variables are operator escape hatches for shutting down a plugin subsystem when something installed has gone bad and you need the daemon to keep running while you investigate. They require a daemon restart to take effect and another restart to re-enable plugin dispatch after you clear the variable.
| Variable | Description |
|---|---|
ANIMUS_DAEMON_DISABLE_TRIGGERS | Truthy (1, true, yes, on) — skips the trigger plugin supervisor on daemon start AND interrupts any in-progress backoff sleeps so a flapping plugin stops respawning immediately. Useful when a trigger plugin is panicking, flooding events, or wedging the daemon's startup. Daemon restart needed to re-enable. |
ANIMUS_DAEMON_DISABLE_SUBJECT_PLUGINS | Truthy — skips subject plugin discovery. Subject plugin calls then behave as if no backend is installed, so most subject operations return not-found/method-not-found. Useful for isolating a broken subject backend. |
ANIMUS_DAEMON_DISABLE_LOG_STORAGE_PLUGIN | Truthy — ignores installed log_storage_backend plugins and uses the in-tree logs/events.jsonl backend. |
ANIMUS_DAEMON_DISABLE_BUDGET_ENFORCEMENT | 1 or true — skips the daemon's budget-cap enforcement housekeeping leg: no rescan, no auto-pause/fail of breaching workflows, no workflow-budget-breach notifier events. Declared caps still record manually via animus cost (which only appends to the scoped breach log, never pauses). Each sweep still records its {enabled:false, last_sweep_at} status so animus daemon health and animus status report the leg as disabled. Daemon restart needed to take effect, like the other kill-switches. |
ANIMUS_DISABLE_HARNESS_HOOKS | Non-empty (any value) — suppresses harness-hook activation entirely: the kernel injector writes no per-session animus-policy.json, no animus-hooks.settings.json, and appends no --settings flag to the provider launch args. Use to disable the agent permission/guardrail spine when debugging a session or when the animus-hook binary is unavailable. Read per session at contract-assembly time (no daemon restart needed). See harness-hooks.md. |
ANIMUS_PROVIDER_DISABLE_PLUGIN | Removed in v0.4.12. Previously forced the SessionBackendResolver to fall back to in-tree provider backends. The in-tree backends were extracted to standalone launchapp-dev/animus-provider-* plugins in v0.4.12, so there is nothing left to fall back to. Setting this variable has no effect. If a provider plugin is misbehaving, uninstall it with animus plugin uninstall --name <name> or remove/quarantine the binary from the plugin directory. |
Error surfaces emit a hint pointing at the right kill-switch when a plugin fails its handshake or exhausts its restart budget, so operators don't have to read the source to find these during an incident.
When a provider plugin is not installed for the tool a workflow phase asks for, the resolver surfaces a hard error like:
Provider plugin 'claude' not installed. Install with:
animus plugin install launchapp-dev/animus-provider-claude --allow-shadow-builtin
Or run: animus plugin install-defaultsThis is intentional — silent fallback to a removed in-tree backend would hide broken or missing plugins during daemon startup.
Opt-in anonymous metrics (v0.5.4)
Animus ships a minimal opt-in telemetry surface. It is disabled by default: the first time you run animus init or animus daemon start in a TTY, the CLI prompts once for consent. The answer is persisted to the user-global config at ~/.animus/config.json (overridable via ANIMUS_CONFIG_DIR) so you never see the prompt again across any project on this machine. The opt-in is intentionally never stored in the project-local .animus/config.json — that file can be committed, and a cloned repo must never carry someone else's consent or point at a third-party endpoint. Non-interactive contexts (no TTY) default to opt-out with no prompt.
Config block (in ~/.animus/config.json)
{
"metrics": {
"enabled": true,
"endpoint": "https://metrics.animus.dev/v1/events",
"batch_interval": "P1D",
"install_id": "9e2c1bcc-4a2f-4f24-bf25-9c0d0d3f6e2c"
}
}| Field | Meaning |
|---|---|
enabled | true = opted in, false = opted out, absent block = never asked. |
endpoint | HTTP receiver for batched event-counter payloads. |
batch_interval | ISO 8601 duration. Default P1D (daily). |
install_id | Opaque random UUID generated on opt-in. Wiped on opt-out. |
Kill switch
| Variable | Description |
|---|---|
ANIMUS_METRICS_DISABLE | Truthy (1, true, yes, on) — suppresses all telemetry regardless of config. Overrides any persisted opt-in. |
Privacy invariants
These properties are enforced at the type level; new event types require updating the closed enums in crates/orchestrator-cli/src/services/metrics/events.rs:
- No file paths, repo names, branch names, or git URLs.
- No prompt content, model responses, agent output, or generated text.
- No environment-variable values.
- No credentials, API keys, or tokens.
- No subject IDs (task ids, requirement ids).
- Every event tag is a compile-time enum variant; payloads cannot carry user-supplied strings.
Payload shape
{
"install_id": "<uuid>",
"animus_version": "0.5.3",
"os": "darwin",
"arch": "aarch64",
"events": [
{
"name": "workflow_started",
"tags": {"workflow_kind": "task"},
"count": 5,
"first_seen": "2026-06-04T10:00:00Z"
}
]
}Counter events are batched on disk under ~/.animus/<repo-scope>/metrics/pending.jsonl; in-flight sends rotate that buffer into flushing-<ts>-<uuid>.jsonl, and successful sends update last-send.txt. Events are flushed by animus daemon metrics flush or the next CLI invocation. Failed sends retry with exponential backoff (3 attempts) before being preserved in the pending queue for the next attempt — CLI/daemon operations are never blocked on the send path.
The pending buffer is intentionally bounded because telemetry is best-effort. Current guards in crates/orchestrator-cli/src/services/metrics/recorder.rs are:
- record-time soft cap: once
pending.jsonlreaches 8 MiB, new events are dropped instead of letting the buffer grow without bound - read/flush hard cap: any single metrics file above 16 MiB is treated as pathological and is not read into memory
- stale
flushing-*.jsonlrecovery: oversized abandoned flush snapshots are deleted instead of being folded back into the next batch - manual reclaim sweep:
animus daemon metrics cleanupsweeps every repo scope under~/.animus/, deleting stale or oversizedflushing-*.jsonlfiles and dropping oversizedpending.jsonlbuffers
This trades away some telemetry during a stalled or racing flush in exchange for keeping ordinary CLI commands memory-safe.
CLI surface
| Command | Effect |
|---|---|
animus daemon metrics status | Show enabled flag, install_id, pending event count, last-send timestamp. |
animus daemon metrics enable | Opt in (no re-prompt). Generates a new install_id if missing. |
animus daemon metrics disable | Opt out and drop any buffered events. |
animus daemon metrics flush | Force-send buffered events (debug). |
animus daemon metrics cleanup | Sweep all scoped metrics dirs for orphaned/oversized flushing snapshots and oversized pending buffers. |
Cost governance and model rates (v0.5.5)
Animus tracks per-workflow + per-phase token + USD spend using the AgentRunEvent::Metadata { cost, tokens } events the provider plugins already emit. The rollups live under ~/.animus/<scope>/cost-state.v1.json (schema animus.cost-state.v1) and budget-exceeded decision records are appended to ~/.animus/<scope>/decisions.jsonl (schema animus.budget-exceeded.v1; inspect with animus cost decisions [--since <dur>]).
Declared budget: caps are enforced by the daemon on its housekeeping cadence: once per heartbeat interval (interval_secs) the sweep rescans run spend (reusing the persisted cost-state.v1.json history so completed runs are not re-read), evaluates caps, and acts on newly crossed ones — per-run breach decision in runs/<run_id>/decisions.jsonl, scoped fleet record, the declared on_exceed action (pause goes through the standard workflow pause path, fail fails the current phase terminally, warn records only), and one workflow-budget-breach notifier event per breach. Enforcement is not mid-phase token-by-token: a phase in flight can overshoot by up to one sweep before the pause lands. With no daemon running, animus cost commands record breaches to the scoped log but never pause anything.
When a provider does not report cost on the metadata frame, the aggregator estimates USD from a small per-model rate table (crates/orchestrator-cli/src/services/cost/model_rates.rs). The table is intentionally simple — a single combined input + output + reasoning rate per model family — and reflects published vendor pricing as of the release that contains this file.
| Model prefix | USD per 1M combined tokens |
|---|---|
claude-opus | 30.00 |
claude-sonnet | 6.00 |
claude-haiku | 1.25 |
codex | 5.00 |
o4 | 5.00 |
gpt-5 | 5.00 |
gemini-3 | 1.25 |
gemini-2 | 0.75 |
kimi | 1.00 |
minimax | 0.70 |
opencode | 2.50 |
Unknown model prefixes produce None (no estimate) rather than a fabricated number. Vendor-reported cost on the wire always wins over the table. To declare cost caps, see the budget: section in workflow-yaml.md. To inspect spend, see cli/index.md.
Scheduler wake model
The daemon's main loop is event-driven; the legacy fixed-interval polling loop is gone. A dispatch pass runs when any of these fire:
- Events (
daemon/nudge) — the CLI write pathsanimus subject create/update/statusandanimus queue enqueue/release(and their MCP equivalents, which execute the same CLI handlers) send a best-effortdaemon/nudgecontrol message after a successful mutation. The same wake fires in-process for control-socket writes (web UI), on workflow/phase completion events from running workflow-runners, and after a workflow-config hot-reload. Nudges are fire-and-forget: if the daemon is not running, or predates thedaemon/nudgemethod, the sender silently ignores the failure and the next heartbeat picks the work up. Bursts coalesce — N rapid nudges produce at most one extra pass. A nudge wakes the scheduler immediately, but the resulting dispatch waits for the current tick's in-flight work to finish before the new pass runs — so end-to-end pickup is typically sub-second, bounded by in-flight tick work, not an instantaneous guarantee. - Cron deadlines — the loop computes the earliest upcoming occurrence across all compiled
schedules:and sleeps until exactly that instant, so cron fires on time instead of on the next polling tick. The deadline is recomputed every pass (and a config reload nudges the loop awake), so schedule edits take effect immediately. The catch-up scan (10-minute lookback) remains the recovery path for occurrences missed while the daemon was busy or down, and while any schedule is enabled the loop additionally sweeps at least every 5 minutes (half the catch-up horizon) so a fire blocked by a full pool is retried before it falls out of the horizon, even wheninterval_secsis much longer. - Queue deferred deadlines — when the queue holds deferred entries (
animus queue enqueue --at), the loop asks the queue plugin (queue/next_deadline) for the earliest futurerun_atand folds it into the same timed wake as the cron deadline (min(cron, queue)), so a deferred entry dispatches on time instead of waiting for the heartbeat. - Fallback heartbeat (
interval_secs) — the maximum sleep when no event arrives. This is not the dispatch latency; it bounds pickup of out-of-band state mutations made without the CLI/MCP surfaces, and it paces housekeeping: the heavier reconciliation legs (manual-timeout, zombie-workflow, stale in-progress sweeps) run at most once per heartbeat period, while event wakes run only the dispatch legs (cron schedules, queue drain, completed-process reaping).
The daemon is queue-only: dispatch executes explicitly enqueued work (leased as agent slots free) plus cron schedules. It does not scan the subject backend for Ready tasks — the daemon.auto_run_ready setting and --auto-run-ready flag were removed.
Pause/resume gating is unchanged: animus daemon pause still gates dispatch on event wakes, not just heartbeat ticks — a nudge while paused wakes the loop but dispatches nothing.
Config sources (v0.6)
As of v0.6 the kernel does not parse .animus/*.yaml in its runtime load path. The project's base WorkflowConfig (and therefore the agent-runtime config derived from it) is sourced exclusively by an installed config_source plugin. The daemon requires one (RequiredRole::ConfigSource is part of daemon_default()):
launchapp-dev/animus-config-yaml— the default, reads.animus/workflows.yaml.animus/workflows/*.yamlexactly as the kernel used to (env/secret interpolation, multi-file merge). Installed byanimus plugin install-defaults.
launchapp-dev/animus-config-postgres(or any other) — sources the config from a database / API instead of files.
This is the v0.6 "config sourcing is a plugin" thesis: the YAML parser still ships in orchestrator-config as a library (the animus-config-yaml plugin links it), but the kernel's daemon/CLI load path no longer calls it directly.
How it works
The kernel's ConfigSourceClient calls orchestrator_plugin_host::discover_by_kind for "config_source" on each config load, spawns the plugin (forwarding the daemon's full environment so non-secret ${VAR} interpolation still resolves, plus the manifest-declared secret env), and issues a config/load JSON-RPC call with { project_root, repo_scope }. The plugin returns a ConfigLoadResponse:
config— aConfigModelenvelope:{ schema: "animus.workflow-config.v2", version: 2, config: <WorkflowConfig as opaque JSON> }cache_token—{ version: "<opaque string>", external_inputs: bool }. The plugin owns its own cache token; the kernel re-issuesconfig/loadon each reload.
When no config_source plugin is installed, resolve_plugin_base returns Ok(None) and load_workflow_config_with_metadata returns an actionable error instructing the operator to animus plugin install launchapp-dev/animus-config-yaml (or animus plugin install-defaults). The daemon refuses to start in this state (preflight).
The kernel still owns the compiler: pack-overlay merge, agent-runtime derivation, state-machine compilation, and validation all stay in-kernel regardless of which source produced the base WorkflowConfig.
Hot-reload / reload model
Hot-reload works today for every config source, including DB/API-backed ones: the daemon re-issues config/load on its reload cadence (interval heartbeat, daemon/nudge, an explicit animus workflow config reload, or a config edit), so a changed Postgres/API source is picked up on the next reload pass without any file to watch — then recompiled and broadcast as the standard config_reloaded event on workflow/events.
For instant reload, a config_source plugin may advertise the optional config_watch capability and push config/changed notifications when its source mutates; the daemon subscribes and reloads on receipt instead of waiting for the next cadence pass. Neither animus-config-yaml nor animus-config-postgres advertises config_watch today (they rely on the cadence re-issue above); the push path is available for future watch-capable sources.
Optional config/validate
A plugin may implement the optional config/validate method, which runs a plugin-side syntactic pre-check (e.g. YAML diagnostics with file + line numbers) and returns structured ConfigDiagnostic entries. The kernel still runs the authoritative validate_workflow_config_with_project_root; config/validate is for richer error locality at the source. Plugins that do not implement it return METHOD_NOT_SUPPORTED — the kernel ignores the response and compiles on.
Preflight
RequiredRole::ConfigSource is defined in the preflight enum (v0.6) and is satisfied by any installed config_source plugin. It is not yet required by the daemon's default preflight set (daemon_default()) — the in-tree YAML path remains the default acquisition and no config_source plugin is required for an existing project to start. The role will be added to daemon_default() once a reference animus-config-yaml plugin is published as the default-flavor impl. In the meantime animus daemon preflight does not report this role as missing, and --skip-preflight is not needed for YAML-only projects.
animus-config-postgres (the LaunchApp portal, running a from-source v0.6 binary) is the first production config_source plugin. The general-audience release of the role tracks the publication of launchapp-dev/animus-config-yaml.
Optional conversation_store role
Chat history (animus chat) is served by an optional conversation_store plugin role. With no plugin installed, the in-tree filesystem store under ~/.animus/<repo-scope>/chat/ is used, so chat works with zero plugins. When a conversation_store plugin is discovered (discover_by_kind(.., "conversation_store")), the chat data ops route to it over JSON-RPC instead — this is how an out-of-tree Postgres backend serves chat history with per-user ownership (owner) and sharing (visibility). Unlike config_source, this role is never a RequiredRole: it is not in daemon_default(), animus daemon preflight never reports it, and the daemon never refuses to start without it. The contract lives in the conversation_store module of crates/animus-plugin-protocol. See docs/reference/chat.md for the full surface.
Notes
- Project YAML is the authored workflow surface.
- As of v0.6 (kernel purification) the kernel ships ZERO built-in pack/workflow content: no baked agent-runtime defaults (the merge base is structurally empty), an empty workflow
phase_catalog, and nohello-worldwalkthrough template. The kernel keeps only its OWNmcp_servers["animus"]structural entry. All phases/agents/workflows come from installed packs and theconfig_source-sourced workflow overlay. A freshanimus initwalkthrough installs the recommended packs to become functional. The project-local.animus/workflows/*.yamlscaffold written by init defines a minimal, self-contained starter set you can edit or replace. animus init --jsonnow includes arecommended_installobject sourced fromcrates/orchestrator-cli/config/default-install.jsonso callers can surface the default pack and plugin set explicitly.- Mutable runtime state lives under
~/.animus/<repo-scope>/. - The daemon schedules and supervises work; workflow and pack content still define behavior.