Environment variables
Two categories:
Injected into the agent — what your script sees when dotagent invokes it.
Read by dotagent itself — overrides for paths, verbosity, OTel headers.
Injected into the agent subprocess
When dotagent spawns your agent, these AGENT_* variables are set on top of the inherited environment (unless env.inherit = false in the manifest). They are the only API surface you depend on — there's no SDK to import.
AGENT_NAME
string
finops-weekly
Always.
AGENT_HOME
abs path
/Users/me/.config/dotagent/agents/finops-weekly
Always — the manifest directory.
AGENT_TMPDIR
abs path
/var/folders/.../tmpAbCdEf
Always — fresh per run, auto-cleaned on exit.
AGENT_DRY_RUN
"true" / "false"
"false"
Always.
AGENT_SCHEDULE_ID
string
daily
Always — matches [[schedules]].id.
AGENT_SLUG
string
period_dia-anterior
Always — derived from the schedule's args.
AGENT_START_EPOCH
int
1700000000
Always — unix epoch of started_at.
AGENT_ARGV
JSON array
["--period","dia-anterior"]
Always — the schedule's args as JSON.
AGENT_HEARTBEAT_FILE
abs path
~/.config/dotagent/state/agents/.../slug.heartbeat.json
Set when NOT dry-run.
AGENT_LIFECYCLE
string
persistent
Only when [lifecycle] mode = "persistent". Absent otherwise, so one script can support both shapes.
AGENT_PERSIST_KEY
string
12345
Persistent runs only — which slice this instance answers for (the resolved [lifecycle] key, or default).
AGENT_TRIGGER_SOURCE
string
telegram
One-shot triggered runs only. One of telegram, local, mcp, cli. Never set in persistent mode — see below.
AGENT_TRIGGER_ACTOR
string
123456789
One-shot triggered runs, when the source can attest an identity. Telegram: numeric user id.
AGENT_TRIGGER_REPLY_TO
string
123456789
One-shot triggered runs, when the source can be answered. Telegram: chat id.
AGENT_TRIGGER_PAYLOAD
JSON object
{"text":"/standup x","chat_id":1,"user_id":2,"command":{"name":"standup","args":"x"}}
One-shot triggered runs. Body travels here, never in argv. command is present only when the sender invoked one — see Commands. reply_to_run is present when the sender replied to a notification dotagent sent — see below.
AGENT_SESSION_ID
string
chat-9_a
One-shot triggered runs only, when the source has a conversation id. Persistent agents receive this as trigger.session_id in each request frame instead.
AGENT_ASSISTANT_CONTEXT_RETIRED
"true"
"true"
Only on the first triggered [assistant] run after automatic transcript retirement. Absent after /novo and otherwise.
LANG
string
en_US.UTF-8
Only when neither LANG nor LC_ALL was inherited — see below.
LANG, and why a daemon has to name one
launchd and systemd start a daemon with no locale at all, and every agent inherits that gap. A process in the resulting C locale has MB_CUR_MAX == 1: it reads each byte of an environment variable as one character (Latin-1) and writes it back out as UTF-8. AGENT_TRIGGER_PAYLOAD carrying é (c3 a9) reaches the agent as é (c3 83 c2 a9) — still valid UTF-8, so nothing errors and the agent just acts on mangled text.
So dotagent fills the gap: en_US.UTF-8 on macOS, C.UTF-8 elsewhere. Only when the value is missing — an inherited LANG or LC_ALL is never overridden, and [env.extra] wins over both:
Reproduced with fish 3.7.1, whose command-substitution output is not affected — which is why an agent could log a mangled prompt beside a clean answer and look like a model problem.
reply_to_run
When the message answers a notification dotagent sent, the payload carries which run it came from:
Resolved from the replied-to message id within the inbound chat, not from the text. The wording is not a stable interface — one event names agent/schedule and another says only preflight aborted by plugin preflight-warp — and two agents can fail identically. Absent when the reply is to something else, when the notification is older than the few hundred kept in state/notify/telegram/sent.json.
Persistent agents get no per-message trigger environment
An environment is fixed at spawn. A persistent process is spawned once and answers many different messages, so those per-message variables would freeze the first one and keep serving it — and stale trigger context reads as perfectly valid, which is worse than absent.
In [lifecycle] mode = "persistent" the same information arrives in the trigger field of each request frame:
The request's trigger object carries source, optional session_id, actor, reply_to, and payload. session_id is per-trigger context, not a process-wide conversation store. The local one-shot API validates local session ids against ^[A-Za-z0-9_-]{1,64}$ and defaults an omitted id to default. See Local Client API.
AGENT_TMPDIR also changes lifetime: it belongs to the instance rather than to one request, so it survives between them and is removed when the instance is recycled. See the persistent protocol.
The positional argv of your process is [run].command + [run].args
schedule's
args, so most scripts don't actually needAGENT_ARGVunless they want JSON-shaped access.
Slug derivation
AGENT_SLUG is computed from the schedule's args:
args
slug
[]
default
["--period", "dia-anterior"]
period_dia-anterior
["--mode", "unsubscribe"]
mode_unsubscribe
["foo bar"]
foo_bar
Rules: strip leading dashes, lowercase, replace non-alphanumeric with _, collapse repeated _, trim trailing _. Empty input → default.
For triggered runs, AGENT_SLUG is the source slug rather than the schedule slug. Without a session it is trigger-<source>; with a session it is trigger-<source>-<sanitized-session>. The local API uses its effective default session when the request omits session_id.
Reading these vars
Fish:
Python:
Go:
Bash:
Extra variables you declare
[env].extra in the manifest is merged on top. Standard idiom for agent-tuning constants:
Set inherit = false if you want a hermetic environment (no parent env leaks in — careful, that removes $PATH too unless you re-add it under extra).
Injected into a skill script
A skill may package executables under scripts/, which skill-run executes. Those get a different, much smaller set of variables — a skill script is not an agent run: it has no schedule, no heartbeat and no manifest.
SKILL_NAME
string
triage
Always.
SKILL_DIR
abs path
/Users/me/.config/dotagent/skills/triage
Always — also the working directory.
Everything else in the environment is inherited from whatever spawned dotagent mcp. Arguments arrive through argv, never a shell string.
Read by dotagent itself
dotagent reads these to override defaults. None are required — everything works out-of-the-box.
DOTAGENT_HOME
Override the root directory.
~/.config/dotagent
DOTAGENT_ROOT
Extra (colon-separated) directories to scan for manifests, prepended to the default search list.
(empty)
DOTAGENT_PLUGIN_PATH
Extra (colon-separated) directories to search for plugin binaries.
(empty)
RUST_LOG
Tracing filter — overrides [logging].level in config.toml.
info
DOTAGENT_LOG_STDERR
Force the stderr mirror layer on (1/true/yes/on) or off (0/false/no/off).
(unset — mirror follows TTY)
NO_COLOR
Set to any value to suppress ANSI escapes in the daemon and every subcommand.
(unset)
OTEL_EXPORTER_OTLP_HEADERS
OTLP auth headers (comma-separated k=v). Used when [telemetry].otlp_endpoint is set.
(empty)
DOTAGENT_HOME
Moves everything dotagent owns (manifests, state, logs, audit, config) under a different root.
Both the daemon and CLI must agree — if you start the daemon with DOTAGENT_HOME=A and then run dotagent reload with DOTAGENT_HOME=B, the reload reads the wrong PID file (the daemon under A's state/daemon.pid).
When set in a launchd plist:
When set for a systemd unit:
DOTAGENT_ROOT
Adds search roots for manifest discovery, ahead of the defaults. Useful for CI / testing without touching ~/.config/dotagent/:
The full discovery order with DOTAGENT_ROOT set:
Every directory in
$DOTAGENT_ROOT$DOTAGENT_HOME/agents/$CWD/agents/$CWD
Each direct subdirectory of these roots that contains an agent.toml becomes one agent. Duplicates resolve to first-found by agent.name.
DOTAGENT_PLUGIN_PATH
Adds search directories for plugin binary resolution, ahead of the defaults:
Full plugin discovery order:
Every directory in
$DOTAGENT_PLUGIN_PATH$DOTAGENT_HOME/plugins//usr/local/lib/dotagent/plugins/$PATH
First match wins. dotagent plugin list shows the resolved path.
Daemon gotcha: when launchd starts the daemon, your interactive shell's
$DOTAGENT_PLUGIN_PATHis NOT inherited. Set it in the plist (EnvironmentVariables), or move the plugin into$DOTAGENT_HOME/plugins/.
RUST_LOG
Overrides [logging].level from config.toml. Same EnvFilter syntax as the rest of the Rust ecosystem — per-target filters supported:
The RUST_LOG env var only affects the CLI subcommand it's set for. To make it sticky for the daemon, put it in the launchd plist / systemd unit Environment=.
DOTAGENT_LOG_STDERR
Whether the daemon mirrors its tracing stream to stderr in addition to logs/daemon/dotagent.log.
The default is only when stderr is a terminal. Run dotagent daemon by hand and you get the familiar compact stream; run it under launchd or systemd and you do not — there stderr is an appended plain file (run.avelino.dotagent-error.log) that no rotation policy covers, so the mirror would duplicate an already-rotated log into one that grows forever. What still reaches that file is what has nowhere else to go: panics, and startup failures that happen before logging is up.
Accepted: 1, true, yes, on / 0, false, no, off. Anything else is ignored and the TTY default applies.
NO_COLOR
Set to any value (including empty) and dotagent emits no ANSI escapes — in the daemon's stderr mirror and in every subcommand's output. Colour is otherwise enabled only when the stream is a terminal, so redirecting to a file already gives you plain text. Follows no-color.org.
OTEL_EXPORTER_OTLP_HEADERS
Vendor-specific authentication for the OTLP exporter. Format is comma-separated k=v. Used only when [telemetry].otlp_endpoint is non-empty in config.toml.
Headers can also be declared in [telemetry.headers] of config.toml — the env var wins when both are set.
See observability.md for full vendor recipes.
What dotagent does NOT honor
Some env vars you might expect from other tools — dotagent ignores them on purpose:
XDG_CONFIG_HOME
dotagent uses ~/.config/dotagent directly. Override via DOTAGENT_HOME.
XDG_DATA_HOME
dotagent doesn't separate config/data/cache — everything lives under DOTAGENT_HOME.
EDITOR
dotagent has no interactive editing.
LAUNCH_AGENT
The daemon is the launchd-managed unit; agents themselves never touch launchd.
Quick reference
Related
paths.md— where every file actually livesagent-spec.md—[env]block in the manifestconfig-reference.md—config.tomloptions that env vars can overrideobservability.md—RUST_LOGand OTel headers in context
Last updated
Was this helpful?