Configuration

May 26, 2026 Β· View on GitHub

Untether reads configuration from ~/.untether/untether.toml.

If you expect to edit config while Untether is running, set:

=== "untether config"

```sh
untether config set watch_config true
```

=== "toml"

```toml
watch_config = true
```

Top-level keys

KeyTypeDefaultNotes
watch_configboolfalseWatch config file for changes; applies most settings immediately. See Hot-reload vs restart-required below.
default_enginestring"codex"Default engine id for new threads.
default_projectstring|nullnullDefault project alias.
transportstring"telegram"Transport backend id.

Hot-reload vs restart-required

When watch_config = true, Untether watches untether.toml and applies most changes immediately. A handful of settings require a process restart because they're bound to resources (network sockets, bot token, session-mode machinery) that can't be swapped live.

Fields listed as restart-required trigger a warning in the Telegram chat (πŸ”„ prefix) AND a structlog config.reload.transport_config_changed record when edited. Everything else hot-reloads silently with a matching config.reload.transport_config_hot_reloaded INFO event.

The authoritative list lives on each settings model as RESTART_REQUIRED_FIELDS (see src/untether/settings.py) so code, docs, and UI can't drift. Editing untether.toml to update one of these while the service runs logs the warning and sends the Telegram notice, but the new value won't take effect until you restart.

SectionRestart-required fieldsHot-reload
transports.telegrambot_token, chat_id, session_mode, topics, message_overfloweverything else (voice_*, show_resume_line, forward_coalesce_s, media_group_debounce_s, allowed_user_ids, files.*)
transports.telegram.topicswhole section (treated as one unit)β€”
top-level transportchanging transport idβ€”
triggersenabled (master switch initialises the cron scheduler + webhook server at startup); server.host, server.port (socket bind at startup)cron add/remove/edit, webhook add/remove/edit, rate_limit, max_body_bytes, default_timezone, per-cron timezone/run_once/permission_mode

To restart:

systemctl --user restart untether        # staging
systemctl --user restart untether-dev    # dev

transports.telegram

=== "untether config"

```sh
untether config set transports.telegram.bot_token "..."
untether config set transports.telegram.chat_id 123
```

=== "toml"

```toml
[transports.telegram]
bot_token = "..."
chat_id = 123
```
KeyTypeDefaultNotes
bot_tokenstring(required)πŸ”„ Telegram bot token from @BotFather. Restart-required.
chat_idint(required)πŸ”„ Default chat id. Restart-required.
allowed_user_idsint[](required, non-empty)Allowed sender user ids. Required for security as of v0.35.3 (#377) β€” set to a non-empty list of Telegram user IDs (your own user id is the typical minimum). An empty list now triggers a hard ConfigError at startup unless you opt in to allow_any_user = true (see below).
allow_any_userboolfalseDev/demo escape hatch (#377). Set to true to keep the prior insecure-default behaviour where any Telegram user who knows the bot username can send commands. Logged at INFO on every boot (security.allow_any_user) so the deviation is visible in journalctl. Use only for hackathons, demos, or local dev.
message_overflow"trim"|"split""split"πŸ”„ How to handle long final responses. Restart-required.
forward_coalesce_sfloat1.0Quiet window for combining a prompt with immediately-following forwarded messages; set 0 to disable.
voice_transcriptionboolfalseEnable voice note transcription.
voice_max_bytesint10485760Max voice note size (bytes).
voice_transcription_modelstring"gpt-4o-mini-transcribe"OpenAI transcription model name.
voice_transcription_base_urlstring|nullnullOverride base URL for voice transcription only.
voice_transcription_api_keystring|nullnullOverride API key for voice transcription only.
session_mode"stateless"|"chat""stateless"πŸ”„ Auto-resume mode. See workflow modes β€” "chat" for assistant/workspace, "stateless" for handoff. Restart-required.
show_resume_linebooltrueShow resume line in message footer. See workflow modes β€” false for assistant/workspace, true for handoff.

When allowed_user_ids is set, updates without a sender id (for example, some channel posts) are ignored.

transports.telegram.topics

πŸ”„ Restart-required as a whole section β€” changes to either key only take effect after a restart because topic initialisation runs at startup.

KeyTypeDefaultNotes
enabledboolfalseπŸ”„ Enable forum-topic features. Restart-required.
scope"auto"|"main"|"projects"|"all""auto"πŸ”„ Where topics are managed. Restart-required.

transports.telegram.files

KeyTypeDefaultNotes
enabledboolfalseEnable /file put and /file get.
auto_putbooltrueAuto-save uploads.
auto_put_mode"upload"|"prompt""upload"Whether uploads also start a run.
uploads_dirstring"incoming"Relative path inside the repo/worktree.
allowed_user_idsint[][]Allowed senders for file transfer; empty allows private chats (group usage requires admin).
deny_globsstring[](defaults)Glob denylist (e.g. .git/**, **/*.pem).
outbox_enabledbooltrueEnable agent-initiated file delivery via .untether-outbox/. Requires enabled = true.
outbox_dirstring".untether-outbox"Relative outbox directory name (must not be absolute).
outbox_max_filesint (1–50)10Max files sent per run.
outbox_cleanupbooltrueDelete sent files and remove empty outbox directory after delivery.

File size limits (not configurable):

  • uploads: 20 MiB
  • downloads / outbox: 50 MiB

projects.<alias>

=== "untether config"

```sh
untether config set projects.happy-gadgets.path "~/dev/happy-gadgets"
untether config set projects.happy-gadgets.worktrees_dir ".worktrees"
untether config set projects.happy-gadgets.default_engine "claude"
untether config set projects.happy-gadgets.worktree_base "master"
untether config set projects.happy-gadgets.chat_id -1001234567890
```

=== "toml"

```toml
[projects.happy-gadgets]
path = "~/dev/happy-gadgets"
worktrees_dir = ".worktrees"
default_engine = "claude"
worktree_base = "master"
chat_id = -1001234567890
```
KeyTypeDefaultNotes
pathstring(required)Repo root (expands ~). Relative paths are resolved against the config directory.
worktrees_dirstring".worktrees"Worktree root (relative to path unless absolute).
default_enginestring|nullnullPer-project default engine.
worktree_basestring|nullnullBase branch for new worktrees.
chat_idint|nullnullBind a Telegram chat to this project.

Legacy config note: top-level bot_token / chat_id are auto-migrated into [transports.telegram] on startup.

Plugins

plugins.enabled

=== "untether config"

```sh
untether config set plugins.enabled '["untether-transport-slack", "untether-engine-acme"]'
```

=== "toml"

```toml
[plugins]
enabled = ["untether-transport-slack", "untether-engine-acme"]
```
  • enabled = [] (default) means β€œload all installed plugins”.
  • If non-empty, only distributions with matching names are visible (case-insensitive).

plugins.<id>

Plugin-specific configuration lives under [plugins.<id>] and is passed to command plugins as ctx.plugin_config.

Controls what appears in the message footer after a run completes.

=== "toml"

```toml
[footer]
show_api_cost = false
show_subscription_usage = true
```
KeyTypeDefaultNotes
show_api_costbooltrueShow the API cost/tokens line (πŸ’°).
show_subscription_usageboolfalseShow 5h/weekly subscription usage (⚑). Claude Code engine only.

When show_subscription_usage is enabled, a compact line like ⚑ 5h: 45% (2h 15m) | 7d: 30% (4d 3h) appears after every Claude Code run. Threshold-based warnings (β‰₯70%) appear regardless of this setting.

preamble

Controls the context preamble injected at the start of every agent prompt.

=== "toml"

```toml
[preamble]
enabled = true
text = "Custom preamble text..."
```
KeyTypeDefaultNotes
enabledbooltrueInject preamble into prompts.
textstring|nullnullCustom preamble text. null uses the built-in default.

The default preamble tells agents they're running via Telegram, lists key constraints (only assistant text is visible), and requests a structured end-of-task summary.

progress

Controls progress message rendering during agent runs.

=== "toml"

```toml
[progress]
verbosity = "verbose"
max_actions = 8
heartbeat_interval = 30
```
KeyTypeDefaultNotes
verbosity"compact" | "verbose""compact"compact shows status + title only. verbose adds tool detail lines (file paths, commands, patterns).
max_actionsint (0–50)5Maximum action lines shown in the progress message.
heartbeat_intervalint (5–120)30Heartbeat tick that re-renders progress messages so long-running tools surface an elapsed-time tail (e.g. β–Έ Bash Β· 3m 47s Β· npm run build) without waiting for the next JSONL event (#481).

Per-chat override: /verbose on and /verbose off override the config default for the current chat without editing the TOML file. /verbose clear removes the override.

!!! tip "Hot-reload" Editing [progress] in untether.toml applies on the next run without restart (#269). The default presenter and per-chat /verbose overrides both pick up the new values.

cost_budget

=== "toml"

```toml
[cost_budget]
enabled = true
max_cost_per_run = 2.00
max_cost_per_day = 10.00
warn_at_pct = 70
auto_cancel = false
```
KeyTypeDefaultNotes
enabledboolfalseEnable cost budget tracking.
max_cost_per_runfloat|nullnullPer-run cost limit (USD).
max_cost_per_dayfloat|nullnullDaily cost limit (USD).
warn_at_pctint70Warning threshold (0–100).
auto_cancelboolfalseAuto-cancel runs that exceed the per-run limit.

Budget alerts always appear regardless of [footer] settings.

!!! note "Cumulative session cost is not capped" Sessions can stack many runs via /continue, follow-up prompts, or a long back-and-forth dogfooding chat. A single session has been observed to cumulate over US$100 across 5 sub-runs even though each individual run was well under max_cost_per_run (#517). If you need a ceiling that spans sessions, set max_cost_per_day β€” Untether tracks daily cost across all sessions and triggers cost_budget.exceeded once the day's spend crosses it. A max_cost_per_session knob is not currently provided; file a feature request if your workflow needs one.

watchdog

=== "toml"

```toml
[watchdog]
liveness_timeout = 600.0
stall_auto_kill = false
stall_repeat_seconds = 180.0
tool_timeout = 600.0
mcp_tool_timeout = 900.0
detect_stuck_after_tool_result = false
stuck_after_tool_result_timeout = 300.0
stuck_after_tool_result_recovery_enabled = true
stuck_after_tool_result_recovery_delay = 60.0
detect_catalog_staleness = true
notify_catalog_refresh = false
prespawn_ram_warn_mb = 2000
prespawn_ram_block_mb = 500
claude_stream_idle_timeout_ms = 300_000
post_result_idle_enabled = true
post_result_idle_timeout = 600.0
bash_grace_seconds = 60.0
```
KeyTypeDefaultNotes
liveness_timeoutfloat600.0Seconds of no stdout before subprocess.liveness_stall warning (60–3600).
stall_auto_killboolfalseAuto-kill stalled processes. Requires zero TCP + CPU not increasing.
stall_repeat_secondsfloat180.0Interval between repeat stall warnings in Telegram (30–600).
tool_timeoutfloat600.0Stall threshold (seconds) for running local tool calls like Bash, Read, Write (60–7200). Increase for long builds or benchmarks.
mcp_tool_timeoutfloat900.0Stall threshold (seconds) for running MCP tool calls (60–7200). MCP tools are network-bound and may legitimately run for 10–20+ minutes.
detect_stuck_after_tool_resultboolfalseEnable the stuck-after-tool_result detector (#322) β€” fires when a tool_result arrives and the engine goes silent for stuck_after_tool_result_timeout seconds while CPU-active (matches the upstream Claude-code / mcp-remote / undici wedge). Opt-in this release; will default true once the recovery path has more staging soak time. The detector is suppressed during legitimate long-running background primitives (Monitor, Bash run_in_background=true, Agent run_in_background=true, ScheduleWakeup, RemoteTrigger) via the per-session tracking infrastructure (#346 / #347).
stuck_after_tool_result_timeoutfloat300.0Seconds of silence after a tool_result before the detector fires (60–1800). Matches undici's default idle-body timeout.
stuck_after_tool_result_recovery_enabledbooltrueWhen the detector fires, attempt tiered recovery: Tier 2 SIGTERMs mcp-remote/@modelcontextprotocol adapter children (forces the SSE reader to error out and unblocks the parent engine); Tier 3 cancels the run via cancel_event. Set false to log only. Has no effect if detect_stuck_after_tool_result = false.
stuck_after_tool_result_recovery_delayfloat60.0Seconds between Tier 2 MCP-adapter SIGTERM and Tier 3 cancel escalation (10–600).
detect_catalog_stalenessbooltrueMCP catalog observability (#365) β€” emit catalog_staleness.detected structlog WARNING once per (session, server, status) tuple when Claude Code's system.init reports any MCP server with a non-connected status (e.g. pending, failed, needs-auth). Observability only β€” no kill or recovery action. Set false to silence the warning. Claude runner only.
notify_catalog_refreshboolfalseOpt-in experimental (#365) — after each tool_result batch, send an mcp_status control_request on Claude's stdin to nudge the catalog. Documented parent→CLI primitive from Anthropic's claude-agent-sdk-python (get_mcp_status). Logs catalog.refresh_sent INFO on success. Default false because the upstream refresh effect on the catalog UI is empirical; enable on staging to measure. Claude runner only.
prespawn_ram_warn_mbint2000Pre-spawn RAM guard (#350) β€” emit subprocess.prespawn.ram_warning when free RAM is below this threshold (MB) at engine spawn. 0 disables the warn tier.
prespawn_ram_block_mbint500Refuse to spawn the engine subprocess (yields CompletedEvent(ok=False, error="πŸ›‘ Insufficient RAM…")) when free RAM is below this threshold (MB). 0 disables the block tier; 0 for both fully disables the guard. Must be strictly less than prespawn_ram_warn_mb when both are set.
claude_stream_idle_timeout_msint300_000Sets CLAUDE_STREAM_IDLE_TIMEOUT_MS in the Claude Code subprocess env via setdefault (#438). Range 30 s – 30 min. Long-form opus 4.7 1M plan-mode generations can legitimately idle the SSE stream past 5 min; deployments hitting upstream Anthropic API stalls (Type A β€” mid-generation) can raise this to 600_000 or 900_000 to ride out longer silences. Type-B failures (cold-start zero-byte, num_turns ≀ 1 && duration_api_ms == 0) are upstream API outages β€” raising this won't help; the failure error message now classifies both modes inline. Shell-set CLAUDE_STREAM_IDLE_TIMEOUT_MS still wins.
post_result_idle_enabledbooltrueClaude post-result idle watchdog (#333) β€” closes Claude's stdin cleanly after post_result_idle_timeout of silence following a result event so multi-turn sessions don't sit alive (and billable) for the full upstream ~36 min idle window. Set false to disable (Claude will sit idle until the upstream CLI exits on its own). The clean exit is auto-continue safe β€” last_event_type=result is excluded from the auto-continue gate.
post_result_idle_timeoutfloat600.0Seconds the watchdog waits after a result event before closing stdin (30–3600). The first result also emits a βœ“ turn complete footer hint so users know the turn is done; when the watchdog actually fires it sends one Telegram message: βœ“ turn complete Β· session closed after Nm idle. Re-arms (instead of closing) if a control_request or AskUserQuestion is mid-flight, so a button click in flight is never orphaned.
bash_grace_secondsfloat60.0Stall-warning grace window for Bash / BashOutput / KillShell tools (#481). Range 5–300. While the most-recent action is one of these and within this window of its start, stall warnings (and the _STALL_MAX_WARNINGS auto-cancel arm) are suppressed β€” long builds and deploys are an expected wait, not a hung session.

The stall monitor in ProgressEdits fires at 5 min (300s) idle, 10 min for local tools, 15 min for MCP tools, and 30 min for pending approvals. When a local tool is running and the child process is CPU-active, the first stall warning fires but repeat warnings are suppressed β€” they resume if CPU goes idle (indicating a genuinely stuck tool). The liveness watchdog in the subprocess layer fires at liveness_timeout with /proc diagnostics. When stall_auto_kill is enabled, auto-kill requires a triple safety gate: timeout exceeded + zero TCP connections + CPU ticks not increasing between snapshots.

[loop]

Controls Untether's observation of Claude Code's session-scoped scheduling tools (CronCreate, ScheduleWakeup). Off by default β€” users opt in per chat via /config β†’ πŸ” Loop mode. (#289)

=== "toml"

```toml
[loop]
enabled = false
inline_threshold_seconds = 300
redundancy_check_interval = 30
max_iterations = 20
max_total_duration_hours = 4
min_interval_seconds = 60
expiry_days = 7
```
KeyTypeDefaultNotes
enabledboolfalseGlobal default for Loop mode. Per-chat override available via /config β†’ πŸ” Loop mode.
inline_threshold_secondsint300ScheduleWakeup calls with delaySeconds ≀ this stay rendered live by the rc8 countdown β€” no Untether-side timer is registered. Long waits (above the threshold) get an Untether timer that survives subprocess exit.
redundancy_check_intervalint30Seconds the fire path waits before retrying when the originating subprocess is still alive (race-avoidance gate).
max_iterationsint20Runaway-safety cap on iteration count (NOT a cost cap).
max_total_duration_hoursint4Runaway-safety cap on wall-clock duration (NOT a cost cap).
min_interval_secondsint60Minimum interval between fires (matches upstream cron floor).
expiry_daysint7Auto-expire loops 7 days after creation (matches upstream's session-task expiry).

Cost limits are NOT in [loop] β€” they live in [cost_budget] and apply to loop fires automatically. See Cost budgets for setup.

State is persisted to active_loops.json (sibling of your untether.toml) so loops survive restarts. The do-not-resume sentinel for /cancel-cancelled loops is persisted alongside.

[auto_continue]

Auto-continue detects when Claude Code exits after receiving tool results without processing them (upstream bugs #34142, #30333) and automatically resumes the session. Detection is based on a protocol invariant: normal sessions always end with last_event_type=result, while premature exits show last_event_type=user.

Auto-continue is suppressed on signal deaths (rc=143/SIGTERM, rc=137/SIGKILL) to prevent death spirals under memory pressure.

=== "toml"

```toml
[auto_continue]
enabled = true
max_retries = 1
```
KeyTypeDefaultNotes
enabledbooltrueEnable automatic session continuation for Claude Code.
max_retriesint1Maximum consecutive auto-continue attempts per run (1–5).

[security]

Runtime security knobs. Defaults are safe β€” operators only flip these when investigating a leak or opting out of a probe.

=== "toml"

```toml
[security]
env_audit = true
env_extra_allow = ["OP_SERVICE_ACCOUNT_TOKEN", "DOPPLER_TOKEN"]
env_extra_prefix_allow = ["VAULT_", "INFISICAL_"]
```
KeyTypeDefaultNotes
env_auditbooltrueOne-shot /proc/<claude_pid>/environ sample on first system.init (#361). Emits claude.env_audit.leaked_var WARNING per non-allowlisted name observed (dedup per session per name). Reuses utils/env_policy.is_allowed. Linux-only β€” silently no-ops elsewhere or when /proc is unreadable. Set false to opt out (e.g. on hardened hosts where /proc/<pid>/environ reads are sensitive). The companion env -i wrap on Claude exec (#361) is always on and not configurable.
env_extra_allowlist[str][]Per-deployment exact-match additions to the engine-subprocess env allowlist (#409). Use for credential-manager tokens that aren't in the global defaults β€” e.g. ["OP_SERVICE_ACCOUNT_TOKEN", "DOPPLER_TOKEN", "INFISICAL_TOKEN"]. Each entry must match [A-Z_][A-Z0-9_]* (uppercase, digits, underscore; cannot start with a digit). Empty / whitespace / lowercase entries are rejected at config-load time. Currently honoured by the Claude and Pi runners. The audit (env_audit) honours these too, so user-allowed names aren't false-flagged as leaks. Untether emits one env_policy.user_extension INFO log per process at first runner spawn so the addition is visible in journalctl.
env_extra_prefix_allowlist[str][]Like env_extra_allow but for name prefixes β€” convenient for credential-manager families where many vars share a prefix. Examples: ["VAULT_"] admits VAULT_TOKEN, VAULT_ADDR, VAULT_NAMESPACE. Each entry must match the same env-var name shape as env_extra_allow.

Engine-specific config tables

Engines use top-level tables keyed by engine id. Built-in engines are listed here; plugin engines should document their own keys.

codex

KeyTypeDefaultNotes
extra_argsstring[]["-c", "notify=[]"]Extra CLI args for codex (exec-only flags are rejected).
profilestring(unset)Passed as --profile <name> and used as the session title.

=== "untether config"

```sh
untether config set codex.extra_args '["-c", "notify=[]"]'
untether config set codex.profile "work"
```

=== "toml"

```toml
[codex]
extra_args = ["-c", "notify=[]"]
profile = "work"
```

claude

KeyTypeDefaultNotes
modelstring(unset)Optional model override.
allowed_toolsstring[]["Bash", "Read", "Edit", "Write"]Auto-approve tool rules.
extra_argsstring[][]Extra CLI args passed to claude (e.g. ["--chrome"] to opt into the Claude-in-Chrome extension). Flags Untether manages internally (-p, --print, --output-format, --input-format, --resume/-r, --continue/-c, --permission-mode, --permission-prompt-tool) are rejected at config-load.
dangerously_skip_permissionsboolfalseSkip Claude Code permissions prompts.
use_api_billingboolfalseKeep ANTHROPIC_API_KEY for API billing.

=== "untether config"

```sh
untether config set claude.model "claude-sonnet-4-5-20250929"
untether config set claude.allowed_tools '["Bash", "Read", "Edit", "Write"]'
untether config set claude.extra_args '["--chrome"]'
untether config set claude.dangerously_skip_permissions false
untether config set claude.use_api_billing false
```

=== "toml"

```toml
[claude]
model = "claude-sonnet-4-5-20250929"
allowed_tools = ["Bash", "Read", "Edit", "Write"]
extra_args = ["--chrome"]    # e.g. opt into Claude-in-Chrome
dangerously_skip_permissions = false
use_api_billing = false
```

pi

KeyTypeDefaultNotes
modelstring(unset)Passed as --model.
providerstring(unset)Passed as --provider.
extra_argsstring[][]Extra CLI args for pi.

=== "untether config"

```sh
untether config set pi.model "..."
untether config set pi.provider "..."
untether config set pi.extra_args "[]"
```

=== "toml"

```toml
[pi]
model = "..."
provider = "..."
extra_args = []
```

opencode

KeyTypeDefaultNotes
modelstring(unset)Optional model override.

=== "untether config"

```sh
untether config set opencode.model "claude-sonnet"
```

=== "toml"

```toml
[opencode]
model = "claude-sonnet"
```

gemini

KeyTypeDefaultNotes
modelstring(unset)Optional model override, passed as --model.
skip_trustbooltruePass --skip-trust so headless runs work outside ~/.gemini/trustedFolders.json (#471). Gemini CLI rejects runs from any directory not in the trust list β€” even with --approval-mode yolo β€” and there is no interactive prompt path in headless usage. Set false to enforce Gemini's project-local extension/MCP trust gate.

=== "untether config"

```sh
untether config set gemini.model "gemini-2.5-pro"
untether config set gemini.skip_trust true
```

=== "toml"

```toml
[gemini]
model = "gemini-2.5-pro"
skip_trust = true
```

!!! note "Approval mode" Gemini CLI's approval mode (read-only / edit files / full access) is toggled per chat via /config β†’ Approval mode, not the config file. Codex CLI's approval policy (full auto / safe) is similarly toggled via /config β†’ Approval policy. See inline settings.

amp

KeyTypeDefaultNotes
modestring(unset)Execution mode, passed as --mode. Values: deep, free, rush, smart.
modelstring(unset)Display label shown in the message footer. Overridden by mode if both are set.
dangerously_allow_allboolfalsePass --dangerously-allow-all to skip AMP's permission prompts. Default flipped to false in v0.35.3 (#206) β€” set to true only if you specifically want AMP runs without its built-in permission system. Untether's own permission layer (when configured) remains the primary control.
stream_json_inputboolfalsePass --stream-json-input for stdin-based prompt delivery.

=== "untether config"

```sh
untether config set amp.mode "deep"
untether config set amp.dangerously_allow_all true
```

=== "toml"

```toml
[amp]
mode = "deep"
dangerously_allow_all = true
```

Triggers

Webhook and cron triggers that start agent runs from external events. See the full Triggers reference for auth, templating, and routing details.

=== "toml"

```toml
[triggers]
enabled = true

[triggers.server]
host = "127.0.0.1"
port = 9876
rate_limit = 60
max_body_bytes = 1_048_576

[[triggers.webhooks]]
id = "github-push"
path = "/hooks/github"
project = "myapp"
engine = "claude"
auth = "hmac-sha256"
secret = "whsec_abc..."
prompt_template = "Review push to {{ref}} by {{pusher.name}}"

[[triggers.crons]]
id = "daily-review"
schedule = "0 9 * * 1-5"
project = "myapp"
engine = "claude"
prompt = "Review open PRs and summarise status."
```

[triggers]

KeyTypeDefaultNotes
enabledboolfalseMaster switch. No server or cron loop starts when false.
default_timezonestring|nullnullDefault IANA timezone for all crons (e.g. "Australia/Melbourne"). Per-cron timezone overrides.

!!! tip "Hot-reload" When watch_config = true, changes to webhooks, crons, schedules, and timezones are applied automatically without restart. Server settings (host, port, rate_limit) and the enabled toggle still require a restart. See the Triggers reference β€” Hot-reload for details.

[triggers.server]

KeyTypeDefaultNotes
hoststring"127.0.0.1"Bind address. Use a reverse proxy for internet exposure.
portint9876Listen port (1--65535).
rate_limitint60Max requests per minute (global + per-webhook).
max_body_bytesint1048576Max request body size in bytes (1 KB--10 MB).

[[triggers.webhooks]]

KeyTypeDefaultNotes
idstring(required)Unique identifier.
pathstring(required)URL path (e.g. /hooks/github).
projectstring|nullnullProject alias for working directory.
enginestring|nullnullEngine override.
chat_idint|nullnullTelegram chat. Falls back to transport default.
authstring"bearer""bearer", "hmac-sha256", "hmac-sha1", or "none".
secretstring|nullnullAuth secret. Required when auth is not "none".
prompt_templatestring(required)Prompt with {{field.path}} substitutions.
event_filterstring|nullnullOnly process matching event type headers.

[[triggers.crons]]

KeyTypeDefaultNotes
idstring(required)Unique identifier.
schedulestring(required)5-field cron expression.
projectstring|nullnullProject alias for working directory.
enginestring|nullnullEngine override.
chat_idint|nullnullTelegram chat. Falls back to transport default.
promptstring(required)Prompt sent to the engine.
timezonestring|nullnullIANA timezone (e.g. "Australia/Melbourne"). Overrides default_timezone.
run_onceboolfalseFire once then auto-disable in-memory. Re-activates on config reload or restart.