Environment Variables
Sortie supports SORTIE_* environment variable overrides for most configuration fields, with optional .env file loading. Environment variables flow in six distinct directions, each covered in its own section below.
| Section | Direction | When it matters |
|---|---|---|
| Configuration overrides | Parent shell / .env file → config fields | Deploying in containers, CI, cloud-native environments |
| Agent runtime variables | Parent shell → agent subprocess | Before starting Sortie |
$VAR indirection in WORKFLOW.md | Parent shell → config fields at startup | Writing the workflow file |
| Hook subprocess environment | Sortie → hook subprocess | Writing hook scripts and reaction triage scripts |
| MCP server environment | Worker → .sortie/mcp.json → agent runtime → MCP server | Writing custom tools, debugging tool execution |
| Install script variables | Parent shell → install.sh | Installing the binary |
Configuration overrides
Each SORTIE_* environment variable below overrides one WORKFLOW.md configuration field. Set them in the parent shell, in a .env file, or both.
Precedence
Four sources feed configuration, highest priority first:
SORTIE_*environment variables in the real process environment.envfile values (opt-in viaSORTIE_ENV_FILEor--env-file)- WORKFLOW.md front matter YAML
- Built-in defaults
A real env var always beats a .env value for the same key. Both beat whatever the YAML says.
Tracker variables
| Env var | Overrides | Type |
|---|---|---|
SORTIE_TRACKER_KIND | tracker.kind | string |
SORTIE_TRACKER_ENDPOINT | tracker.endpoint | string |
SORTIE_TRACKER_API_KEY | tracker.api_key | string (secret, never logged) |
SORTIE_TRACKER_PROJECT | tracker.project | string |
SORTIE_TRACKER_ACTIVE_STATES | tracker.active_states | csv |
SORTIE_TRACKER_TERMINAL_STATES | tracker.terminal_states | csv |
SORTIE_TRACKER_QUERY_FILTER | tracker.query_filter | string |
SORTIE_TRACKER_HANDOFF_STATE | tracker.handoff_state | string |
SORTIE_TRACKER_NO_CHANGE_STATE | tracker.no_change_state | string |
SORTIE_TRACKER_IN_PROGRESS_STATE | tracker.in_progress_state | string |
SORTIE_TRACKER_COMMENTS_ON_DISPATCH | tracker.comments.on_dispatch | bool (true/false/1/0) |
SORTIE_TRACKER_COMMENTS_ON_COMPLETION | tracker.comments.on_completion | bool |
SORTIE_TRACKER_COMMENTS_ON_FAILURE | tracker.comments.on_failure | bool |
Polling variables
| Env var | Overrides | Type |
|---|---|---|
SORTIE_POLLING_INTERVAL_MS | polling.interval_ms | int |
Workspace variables
| Env var | Overrides | Type |
|---|---|---|
SORTIE_WORKSPACE_ROOT | workspace.root | string (path, ~ expanded) |
SORTIE_WORKSPACE_RETENTION_DAYS | workspace.retention_days | int (days) |
Agent variables
| Env var | Overrides | Type |
|---|---|---|
SORTIE_AGENT_KIND | agent.kind | string |
SORTIE_AGENT_COMMAND | agent.command | string |
SORTIE_AGENT_TURN_TIMEOUT_MS | agent.turn_timeout_ms | int |
SORTIE_AGENT_READ_TIMEOUT_MS | agent.read_timeout_ms | int |
SORTIE_AGENT_STALL_TIMEOUT_MS | agent.stall_timeout_ms | int |
SORTIE_AGENT_STOP_GRACE_MS | agent.stop_grace_ms | int |
SORTIE_AGENT_MAX_CONCURRENT_AGENTS | agent.max_concurrent_agents | int |
SORTIE_AGENT_MAX_TURNS | agent.max_turns | int |
SORTIE_AGENT_MAX_RETRY_BACKOFF_MS | agent.max_retry_backoff_ms | int |
SORTIE_AGENT_MAX_SESSIONS | agent.max_sessions | int |
SORTIE_AGENT_MAX_TOKENS | agent.max_tokens | int |
SORTIE_AGENT_MAX_CONSECUTIVE_ABSENCES | agent.max_consecutive_absences | int |
Top-level variables
| Env var | Overrides | Type |
|---|---|---|
SORTIE_DB_PATH | db_path | string (path, ~ expanded) |
Control variables
These are not config field overrides. They control how overrides are loaded.
| Env var | Purpose | Type |
|---|---|---|
SORTIE_ENV_FILE | Path to a .env file containing SORTIE_* overrides | string |
When --env-file is provided, the CLI resolves the path to absolute and exports it as SORTIE_ENV_FILE in the process environment. This ensures the value is captured by the SORTIE_* prefix scan and propagated to the MCP server, which runs in a different working directory and needs the absolute path to locate the .env file. When both SORTIE_ENV_FILE and --env-file are set, the CLI flag wins.
Type coercion
| Type | Rule | Error behavior |
|---|---|---|
| string | Used as-is | - |
| int | Parsed as an integer, with leading and trailing whitespace trimmed first. | Startup error: config: polling.interval_ms: invalid integer value: abc (from SORTIE_POLLING_INTERVAL_MS) |
| bool | Accepts true, false, 1, 0 (case-insensitive) | Startup error naming the env var and rejected value |
| csv | Comma-separated. Items trimmed. Empty items discarded. Empty string produces an empty list. | - |
A numeral outside the range an integer setting accepts is itself a parse failure, reported the same way as the int row above: config: agent.max_turns: value is outside the range an integer setting accepts, -9223372036854775808 to 9223372036854775807 (from SORTIE_AGENT_MAX_TURNS). See the errors reference for the general form.
A value that parses successfully can still be rejected by configuration validation; the table above covers parse failures only. SORTIE_AGENT_TURN_TIMEOUT_MS is one such field, with the constraint documented in the configuration reference.
Fields not overridable via env
| Field | Reason |
|---|---|
hooks.* (all hook scripts) | Multiline shell scripts do not fit in a single env var |
hooks.timeout_ms | Grouped with hooks for consistency |
agent.max_concurrent_agents_by_state | Complex map structure ({"in progress": 3, "to do": 1}) |
tracker.api_version | No override variable exists; set directly in WORKFLOW.md or via $VAR indirection |
tracker.handoff_evidence | No override variable exists; set directly in WORKFLOW.md. Unlike most of its neighbors in the tracker section, it does not resolve $VAR references either. |
ci_feedback.* | No override variables exist; must be set in WORKFLOW.md |
self_review.* | No override variables exist; verification commands are security-sensitive and must come from version-controlled WORKFLOW.md |
reactions.* (including reactions.label_commands) | No override variables exist; reaction configuration must come from WORKFLOW.md |
dispatch.* | No override variables exist; rule definitions and template paths must come from WORKFLOW.md |
notifications | No override variables exist; backend configuration must come from WORKFLOW.md, though $VAR references inside an entry still resolve |
Extension sections (server, worker, claude-code, etc.) | Plugin-owned configuration; overrides belong to the adapter |
logging.level | Controlled by the --log-level CLI flag |
logging.format | Controlled by the --log-format CLI flag |
.env file support
Loading a .env file is opt-in.
Warning
Sortie does not auto-discover .env files in the working directory. Its working directory is the WORKFLOW.md location, and a .env file placed there could silently alter behavior for any operator who runs sortie from that directory. Always load .env explicitly via SORTIE_ENV_FILE or --env-file.
Enable .env loading with either:
# Via environment variable
export SORTIE_ENV_FILE=/etc/sortie/prod.env
sortie WORKFLOW.md
# Via CLI flag (takes precedence over the env var)
sortie --env-file /etc/sortie/prod.env WORKFLOW.mdFile format:
# /etc/sortie/jira.env
# Comments start with #. Blank lines are ignored.
SORTIE_TRACKER_KIND=jira
SORTIE_TRACKER_ENDPOINT=https://myco.atlassian.net
SORTIE_TRACKER_API_KEY="you@company.com:xpat_abc123def456"
SORTIE_TRACKER_PROJECT=PLATFORM
SORTIE_POLLING_INTERVAL_MS=30000
SORTIE_WORKSPACE_ROOT=~/workspace/sortieGitHub adapter equivalent:
# /etc/sortie/github.env
SORTIE_TRACKER_KIND=github
SORTIE_TRACKER_API_KEY="ghp_your_personal_access_token"
SORTIE_TRACKER_PROJECT=myorg/myrepo
SORTIE_POLLING_INTERVAL_MS=30000
SORTIE_WORKSPACE_ROOT=~/workspace/sortieLinear adapter equivalent:
# /etc/sortie/linear.env
SORTIE_TRACKER_KIND=linear
SORTIE_TRACKER_API_KEY="lin_api_your_personal_api_key"
SORTIE_TRACKER_PROJECT=ENG
SORTIE_POLLING_INTERVAL_MS=30000
SORTIE_WORKSPACE_ROOT=~/workspace/sortieGitea adapter equivalent:
# /etc/sortie/gitea.env
SORTIE_TRACKER_KIND=gitea
SORTIE_TRACKER_ENDPOINT=https://gitea.example.com
SORTIE_TRACKER_API_KEY="your_gitea_access_token"
SORTIE_TRACKER_PROJECT=sortie-ai/sortie
SORTIE_POLLING_INTERVAL_MS=30000
SORTIE_WORKSPACE_ROOT=~/workspace/sortieGitLab adapter equivalent:
# /etc/sortie/gitlab.env
SORTIE_TRACKER_KIND=gitlab
# Omit the endpoint on GitLab.com, which the adapter defaults to.
# Set it only to reach a self-managed instance.
SORTIE_TRACKER_ENDPOINT=https://gitlab.example.com
SORTIE_TRACKER_API_KEY="your_gitlab_access_token"
SORTIE_TRACKER_PROJECT=platform/backend/api-gateway
SORTIE_POLLING_INTERVAL_MS=30000
SORTIE_WORKSPACE_ROOT=~/workspace/sortieRules:
- One
KEY=VALUEper line. No multiline values. #lines and blank lines are ignored.- Optional single or double quotes around values. Outer quotes are stripped, with no escape processing.
- Only keys starting with
SORTIE_are loaded. All other keys are silently ignored. - No variable interpolation within values.
$HOMEin a.envvalue is the literal string$HOME. - Real environment variables always take precedence over
.envvalues. - The
.envfile is re-read on every WORKFLOW.md reload (file change detection). Real env vars require a process restart to change.
CSV encoding for list fields
active_states and terminal_states accept comma-separated values:
SORTIE_TRACKER_ACTIVE_STATES="To Do,In Progress"
SORTIE_TRACKER_TERMINAL_STATES="Done,Won't Do"Each item is trimmed of surrounding whitespace. Empty items (from trailing commas or double commas) are discarded. An empty string produces an empty list.
Interaction with $VAR indirection
When a SORTIE_* override is set for a field, it replaces the YAML value entirely. The $VAR expansion that would normally run on the YAML value is skipped for that field. Values from env overrides are literal: $ characters are not expanded.
Example: WORKFLOW.md has api_key: $MY_TOKEN. If SORTIE_TRACKER_API_KEY=tok$5abc is set, the api_key becomes the literal string tok$5abc. The $MY_TOKEN indirection never executes. The $5 is not expanded.
Path fields (workspace.root, db_path) still receive ~ expansion even when set via env overrides. Only $VAR expansion is skipped.
Agent runtime variables
Agent adapters spawn subprocesses that inherit the full parent process environment. Sortie validates none of these variables: they pass straight through, and if one is missing, the agent subprocess fails, not Sortie. COPILOT_HOME is the one Sortie reads for itself, to locate a file the runtime writes.
That inheritance belongs to a local launch. An agent Sortie starts on a remote host through worker.ssh_hosts gets the remote host’s environment, plus the bounded set described under variables carried to a remote agent.
| Variable | Required by | Description |
|---|---|---|
ANTHROPIC_API_KEY | claude-code adapter (Anthropic direct) | API key for the Anthropic API. The Claude Code CLI reads this on startup. Missing or invalid values cause an authentication error in the agent subprocess. |
CLAUDE_CODE_USE_BEDROCK | claude-code adapter (AWS Bedrock) | Set to 1 to route Claude Code through AWS Bedrock instead of the direct API. |
AWS_ACCESS_KEY_ID | claude-code adapter (AWS Bedrock) | AWS access key. Required when CLAUDE_CODE_USE_BEDROCK=1. |
AWS_SECRET_ACCESS_KEY | claude-code adapter (AWS Bedrock) | AWS secret key. Required when CLAUDE_CODE_USE_BEDROCK=1. |
AWS_REGION | claude-code adapter (AWS Bedrock) | AWS region for Bedrock inference. Required when CLAUDE_CODE_USE_BEDROCK=1. |
CLAUDE_CODE_USE_VERTEX | claude-code adapter (Google Vertex AI) | Set to 1 to route Claude Code through Google Vertex AI. |
ANTHROPIC_VERTEX_PROJECT_ID | claude-code adapter (Google Vertex AI) | GCP project ID. Required when CLAUDE_CODE_USE_VERTEX=1. |
CLOUD_ML_REGION | claude-code adapter (Google Vertex AI) | GCP region. Required when CLAUDE_CODE_USE_VERTEX=1. |
ANTHROPIC_BASE_URL | claude-code adapter (proxy) | Override the Anthropic API base URL. Use for LiteLLM, custom gateways, or corporate proxies. |
COPILOT_GITHUB_TOKEN | copilot-cli adapter | GitHub token dedicated to Copilot CLI. Highest priority among the three token variables the CLI checks. |
GH_TOKEN | copilot-cli adapter | GitHub token shared with the gh CLI. Second priority for Copilot CLI authentication. Also used by many GitHub tooling integrations. |
GITHUB_TOKEN | copilot-cli adapter | GitHub token common in CI environments. Third priority for Copilot CLI authentication. |
COPILOT_HOME | copilot-cli adapter (optional) | Root directory the Copilot CLI writes its per-session state under. Default: ~/.copilot. Sortie reads it too: the adapter resolves <COPILOT_HOME>/session-state/<session id>/events.jsonl, the session-state journal that supplies the run’s token counts. An empty or unset value resolves to the default. |
CODEX_API_KEY | codex adapter | OpenAI API key. Sortie reads it from its own environment and signs the app-server in with it over the adapter’s protocol channel, on a local launch and a remote one alike, when the runtime reports no account of its own. A local subprocess also inherits the variable; a remote one never receives it. With the variable unset, the adapter falls back to whatever credentials the runtime already holds where it runs, such as ~/.codex/auth.json. |
KIRO_API_KEY | kiro adapter | API key the Kiro CLI reads on the headless path. The adapter preflights it at session start (presence plus a usability check), so a missing or invalid credential surfaces as a startup error rather than a hang or a silent empty turn. |
A missing ANTHROPIC_API_KEY is the most common claude-code deployment failure. Sortie starts and polls the tracker normally, but every agent session fails at launch with an auth error. The Sortie logs show a worker exit with exit_type=error; the root cause is only visible in the agent’s stderr output.
For copilot-cli, a missing GitHub token is the equivalent failure. The adapter’s preflight check validates that at least one of COPILOT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN is set, or that gh auth status succeeds. If none are available, the session fails to start with agent_not_found. The Copilot CLI itself implements try-and-fallback across these three variables. Precedence matters only when multiple sources hold different valid tokens.
Classic PATs do not work with Copilot CLI
Copilot CLI requires a fine-grained personal access token (prefix github_pat_) with the Copilot Requests permission enabled. Classic PATs (prefix ghp_) fail authentication silently: the CLI falls through all three token variables and reports no valid credential. OAuth tokens (gho_ from copilot auth login) and GitHub App user-to-server tokens (ghu_) also work. If you see authentication failures despite having a token set, check the token prefix.
For codex, a missing CODEX_API_KEY produces the same pattern as Claude Code. Sortie starts normally, but every agent session fails with an authentication error during the app-server initialization handshake. If CODEX_API_KEY is unset, the adapter attempts to use cached credentials from ~/.codex/auth.json; if those are also absent or expired, the session fails to start with response_error. A remote session works the same way: the login travels as a protocol message over the SSH connection, so the key never enters the remote agent’s environment and never needs to be present on the host.
For opencode, authentication is provider-specific and the adapter does not preflight it. OpenCode resolves credentials from its own environment, auth store, project .env, or opencode.json provider config, while the Sortie adapter injects or overrides a small managed OPENCODE_* set on every run and export subprocess.
| Variable | Purpose | Description |
|---|---|---|
OPENCODE_PERMISSION | Permission policy | Inline JSON permission policy. When opencode.allowed_tools or opencode.denied_tools is configured, Sortie removes any inherited value and writes its managed policy instead. |
OPENCODE_AUTO_SHARE | Session sharing | Auto-share on completion. Sortie-managed runs force this to false. |
OPENCODE_DISABLE_AUTOCOMPACT | Context compaction | Managed by opencode.disable_autocompact. |
OPENCODE_DISABLE_AUTOUPDATE | Self-update | Sortie-managed runs force this to true. |
OPENCODE_DISABLE_LSP_DOWNLOAD | LSP download | Sortie-managed runs force this to true. |
In local mode the adapter injects only the managed OPENCODE_* values above; every provider credential, and any OpenCode config-discovery variable such as OPENCODE_CONFIG, comes from the parent environment or from OpenCode’s own auth and config state, unmanaged by Sortie. A remote session receives those same managed values and nothing else of Sortie’s own, so the provider credentials your model selection needs must already exist on the remote host, or be named under worker.ssh_pass_env.
For kiro, authentication is a single credential. The adapter reads KIRO_API_KEY and validates it before the session starts, so a missing or invalid key surfaces as a startup error. That validation runs on a local launch only. A remote session carries KIRO_API_KEY from Sortie’s environment into the agent’s environment on the host, unvalidated, so a bad key there shows up as a failing turn instead. See the Kiro CLI adapter reference for the credential preflight and headless behavior.
For agent-client-protocol, Sortie manages no credential at all. This kind names no default runtime, so there is no fixed variable to preflight or document here: whichever binary agent.command names reads its own credential from the inherited environment, exactly like every other agent adapter’s subprocess. See the Agent Client Protocol adapter reference for the kind itself, and Gemini CLI or Kiro CLI on that route for what each of those two runtimes actually reads.
Variables carried to a remote agent
Every agent kind declares the environment variables its runtime reads as the credential for its default provider. A launch sent to a remote host through worker.ssh_hosts carries that kind’s declared names from Sortie’s own environment, whenever they are set there, without your listing them anywhere. Here is what each built-in kind declares.
| Agent kind | Carried automatically on a remote launch |
|---|---|
claude-code | ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN |
copilot-cli | COPILOT_GITHUB_TOKEN, GH_TOKEN, GITHUB_TOKEN |
kiro | KIRO_API_KEY |
codex | None. The login travels as a protocol message instead, so CODEX_API_KEY never reaches the remote agent’s environment. |
opencode | None. Provider credentials are OpenCode’s own to resolve. Sortie’s managed OPENCODE_* settings do reach a remote session, on the same delivery path but under neither worker field’s control. |
agent-client-protocol | None. The kind names no default runtime, so it has no fixed credential to declare. |
A variable carried this way overrides the value or login the remote host already holds. If Sortie’s own environment sets one of these names for an unrelated purpose, GITHUB_TOKEN for the tracker while your copilot-cli hosts sign in on their own, name it under worker.ssh_disallow_pass_env to keep the host’s login in effect.
Anything else a remote agent needs from Sortie’s environment, a provider key for OpenCode, a registry token a hook cannot supply, goes in worker.ssh_pass_env by name. Both fields take names and never values, and neither affects a local launch. For the precedence rules, the skipped-value warnings, and the remote host’s dd requirement, see that section.
$VAR indirection in WORKFLOW.md
Selected WORKFLOW.md configuration fields resolve environment variable references at startup. This keeps secrets and deployment-specific values out of the workflow file.
$VAR indirection and SORTIE_* configuration overrides are two ways to supply a field from the environment. With indirection, the workflow file names the variable, for example api_key: $SORTIE_GITEA_TOKEN, and Sortie expands it at startup. With an override, a generic SORTIE_TRACKER_* variable such as SORTIE_TRACKER_API_KEY, set in the shell or a .env file, replaces the field value regardless of the workflow file. When both target the same field, the override wins and $VAR indirection is skipped for that field.
Expansion modes
Three expansion modes exist. The mode depends on the field.
Reference only: Expands only when the entire trimmed value is a variable reference ($VAR or ${VAR}). Mixed content like https://example.com/$VAR is returned unchanged, preventing destructive rewriting of URIs and paths.
Anywhere in string: Expands $VAR and ${VAR} references anywhere in the string, including within larger values.
Path: Expands ~ or ~/ at the start of the value to the user’s home directory, then expands $VAR and ${VAR} references anywhere in the rest of the string.
Fields with $VAR support
| Field | Expansion mode | Example value | Resolves to |
|---|---|---|---|
tracker.endpoint | Reference only | $SORTIE_JIRA_ENDPOINT | https://myco.atlassian.net |
tracker.api_key | Anywhere in string | user@example.com:$SORTIE_JIRA_API_KEY | user@example.com:xyztoken123 |
tracker.project | Reference only | $SORTIE_JIRA_PROJECT | PLATFORM |
tracker.query_filter | Reference only | $SORTIE_JIRA_QUERY_FILTER | labels = 'agent-ready' |
tracker.handoff_state | Reference only | $SORTIE_HANDOFF_STATE | Human Review |
tracker.no_change_state | Reference only | $SORTIE_NO_CHANGE_STATE | Done |
tracker.in_progress_state | Reference only | $SORTIE_IN_PROGRESS_STATE | In Progress |
tracker.api_version | Reference only | $SORTIE_JIRA_API_VERSION | 2 |
workspace.root | Path | ~/workspace/sortie | /home/deploy/workspace/sortie |
db_path | Path | $SORTIE_DB_DIR/sortie.db | /var/lib/sortie/sortie.db |
Fields in the core schema outside this table (agent.kind, agent.max_turns, hook scripts, ci_feedback, self_review, reactions, and dispatch) are treated as literal strings with no expansion.
Adapter pass-through blocks (claude-code, worker, github, and similar top-level blocks named after a kind) and each notifications entry are the exception: every string leaf in those blocks is resolved with the same anywhere-in-string semantics, independently of the table above.
The variable names in the table are user-defined conventions, not Sortie-internal identifiers. For the GitHub adapter, common conventions are $SORTIE_GITHUB_TOKEN or $GITHUB_TOKEN for tracker.api_key (a plain personal access token, not email:token format) and $SORTIE_GITHUB_PROJECT for tracker.project (an owner/repo string). See the GitHub adapter reference for per-field semantics.
For the Linear adapter, the conventions are $SORTIE_LINEAR_API_KEY for tracker.api_key (a Linear personal API key carrying the lin_api_ prefix, sent verbatim in the Authorization header with no Bearer prefix; this is the name sortie validate suggests), and $SORTIE_LINEAR_TEAM_KEY for tracker.project (a Linear team key, such as ENG). See the Linear adapter reference for per-field semantics.
For the Gitea adapter, the conventions are $SORTIE_GITEA_TOKEN for tracker.api_key (a Gitea access token, a 40-character hex string with no identifying prefix, sent verbatim in the Authorization: token <key> header with no Bearer prefix, so surrounding whitespace fails authentication; this is the name sortie validate suggests), $SORTIE_GITEA_ENDPOINT for tracker.endpoint (the instance base URL, required because Gitea is self-hosted and has no default host), and $SORTIE_GITEA_PROJECT for tracker.project (an owner/repo string). See the Gitea adapter reference for per-field semantics.
For the GitLab adapter, the conventions are $SORTIE_GITLAB_TOKEN for tracker.api_key (a GitLab access token, sent verbatim in the PRIVATE-TOKEN header, neither Authorization: Bearer nor Authorization: token, so surrounding whitespace fails authentication; the adapter checks neither prefix nor length, because a GitLab administrator can change the access-token prefix through an application setting and a shape check would reject valid tokens on a customized instance; this is the name sortie validate suggests, and it is the tracker credential; do not confuse it with a bare GITLAB_TOKEN, which Sortie does not read), $SORTIE_GITLAB_ENDPOINT for tracker.endpoint (the instance base URL, optional because the adapter defaults to https://gitlab.com, so set it only to reach a self-managed instance), and $SORTIE_GITLAB_PROJECT for tracker.project (the project’s namespace path, which nests to any depth, such as group/project or group/subgroup/project, or its numeric project ID). See the GitLab adapter reference for per-field semantics.
Behavior when a variable is unset or empty
| Scenario | Behavior |
|---|---|
$VAR resolves to an empty string | The field is treated as missing. For required fields (e.g., tracker.api_key when the adapter declares it required), this is a startup error. |
| The referenced variable does not exist in the environment | Same as empty: an undefined variable expands to an empty string. |
tracker.handoff_state resolves to empty | Startup error: config: tracker.handoff_state: resolved to empty (check environment variable). |
tracker.no_change_state resolves to empty | Startup error: config: tracker.no_change_state: resolved to empty (check environment variable). |
db_path resolves to empty | Startup error: config: db_path: resolved to empty (check environment variable). |
What this is not
$VAR indirection is not general shell expansion. It does not support:
- Command substitution (
$(command)or`command`) - Arithmetic expansion (
$((1+2))) - Default values (
${VAR:-default}) - Glob expansion (
*,?)
Hook subprocess environment
Hook scripts (after_create, before_run, after_run, before_remove) run as subprocesses with a restricted environment. On POSIX systems, hooks execute via sh -c; on Windows, via cmd.exe /C. The full parent process environment is not inherited.
Injected variables
Sortie injects these variables into every hook invocation. They override any same-named variable from the parent environment.
| Variable | Type | Description |
|---|---|---|
SORTIE_ISSUE_ID | string | Stable tracker-internal issue ID. |
SORTIE_ISSUE_IDENTIFIER | string | Human-readable ticket key (e.g., PROJ-123). |
SORTIE_WORKSPACE | string | Absolute path to the per-issue workspace directory. Always the same as the hook’s working directory. |
SORTIE_ATTEMPT | string | Current attempt number as a decimal integer. Starts at 1. Increments on retries. 0 if the attempt count is unavailable. |
SORTIE_SSH_HOST | string | SSH host allocated for this issue. Present only when SSH mode is active (extensions.worker.ssh_hosts is configured and a host was assigned). Absent in local mode. |
after_run hook variables
These variables are injected only during after_run hook invocations.
| Variable | Type | Description |
|---|---|---|
SORTIE_SELF_REVIEW_STATUS | string | Self-review outcome for the current run. Values: "disabled" (self-review not configured), "passed" (review passed), "cap_reached" (iteration cap reached without passing), "error" (review loop encountered a fatal error). Set on all after_run invocations. |
SORTIE_SELF_REVIEW_SUMMARY_PATH | string | Absolute path to .sortie/review_summary.md in the workspace. Contains a human-readable Markdown summary of the review outcome. Absent when self-review did not run or the summary file was not written. |
See Configure self-review for usage examples.
Reaction triage command variables
A reaction’s triage command is not a lifecycle hook, but it runs through the same machinery and so gets the same restricted environment, the same working directory, and the injected variables above. On top of those it receives three of its own.
| Variable | Type | Description |
|---|---|---|
SORTIE_REACTION_KIND | string | Which reaction armed: ci, review, bot-review, or merge-conflict. |
SORTIE_REACTION_INPUT | string | Absolute path to a JSON file describing the subject. Written before the command starts and removed after it returns. |
SORTIE_REACTION_RESULT | string | Absolute path the command writes its answer to. The file does not exist when the command starts. |
Both paths sit in a temporary directory created for the run, outside the workspace. See the triage command reference for the two document schemas and the answers the result file accepts.
Inherited variables
Beyond the injected variables above, hooks inherit two categories from the parent Sortie process:
Platform allowlist: A fixed set of standard infrastructure variables, varying by OS:
- POSIX (Linux, macOS):
PATH,HOME,SHELL,TMPDIR,USER,LOGNAME,TERM,LANG,LC_ALL,SSH_AUTH_SOCK,XDG_RUNTIME_DIR,DBUS_SESSION_BUS_ADDRESS - Windows:
PATH,SYSTEMROOT,COMSPEC,PATHEXT,USERPROFILE,TEMP,TMP,APPDATA,LOCALAPPDATA,HOMEDRIVE,HOMEPATH,USERNAME
XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS locate the invoking user’s systemd user manager and D-Bus session bus. Both carry no secret; a hook or reaction triage command that starts a systemd user service needs them to reach it. See start a service that outlives a hook.
SORTIE_* prefix: All parent environment variables whose names start with SORTIE_ are inherited. This includes any SORTIE_* variables set via configuration overrides. This is the intended mechanism for passing additional values (API tokens, repository URLs, custom flags) into hooks without exposing the full process environment.
Stripped variables
Everything not in the allowlist and not prefixed with SORTIE_ is stripped. This includes:
- Cloud credentials:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,GOOGLE_APPLICATION_CREDENTIALS - API tokens:
JIRA_API_TOKEN,ANTHROPIC_API_KEY,GITHUB_TOKEN - Application config:
DATABASE_URL,REDIS_URL, etc.
This is a security boundary. Hooks run user-authored shell scripts; restricting their environment limits the blast radius of a compromised or buggy hook.
Providing additional values to hooks
Two approaches:
SORTIE_-prefixed variables. Export the value with aSORTIE_prefix in the parent environment. It passes through automatically.export SORTIE_JIRA_API_TOKEN="xyztoken123" export SORTIE_REPO_URL="git@github.com:myorg/myrepo.git" sortie WORKFLOW.mdInside the hook:
git clone "$SORTIE_REPO_URL" .In-hook credential loading. Fetch credentials from external sources inside the script.
source /etc/sortie/hooks-env aws sts get-caller-identity
Override precedence
When the same variable name exists in both the parent environment (via SORTIE_* passthrough) and the injected set, the injected value wins. For example, a parent SORTIE_ISSUE_ID=stale is overwritten by the orchestrator’s current SORTIE_ISSUE_ID for the active issue.
MCP server environment
The MCP tool server (sortie mcp-server) runs as a child process of the agent runtime, not of the Sortie orchestrator. The agent runtime builds the MCP server’s environment from the names in the env field of .sortie/mcp.json: a variable not listed in that block does not reach the server. Where the adapter re-expresses the file rather than handing over its path, a listed name can be delivered as a name alone, its value resolved from the agent runtime’s own process environment: see translated delivery. The worker writes per-session context variables and all SORTIE_*-prefixed process environment variables into this block before launching the agent. It writes the file for every agent kind, but the chain runs end to end only where the adapter delivers those servers to its runtime, either directly as the file’s path or re-expressed in the form that runtime parses. Where it delivers neither, nothing spawns the server and the env block reaches nobody; see delivery by agent kind.
Environment composition
The env block is built in two layers:
SORTIE_*process variables (lower precedence). The worker scans the orchestrator’s process environment and collects every variable whose name starts withSORTIE_. This captures credential variables (e.g.,SORTIE_TRACKER_API_KEY), configuration overrides (e.g.,SORTIE_POLLING_INTERVAL_MS), and any operator-definedSORTIE_*values.Per-session variables (higher precedence). The worker writes the following variables, overriding any same-named key from layer 1:
| Variable | Type | Description |
|---|---|---|
SORTIE_ISSUE_ID | string | Tracker-internal issue ID. Scopes tool operations to the current issue. |
SORTIE_ISSUE_IDENTIFIER | string | Human-readable ticket key (e.g., PROJ-123). Used by tracker_api for project-level scoping. |
SORTIE_WORKSPACE | string | Absolute path to the per-issue workspace directory. |
SORTIE_DB_PATH | string | Absolute path to the Sortie SQLite database. The MCP server opens this in read-only mode for Tier 1 tools that query run history (e.g., workspace_history). This is the same resolved path that the orchestrator uses. If you set SORTIE_DB_PATH as a configuration override, the MCP server receives that same value. |
SORTIE_DISPATCH_ID | string | Opaque identifier for the current dispatch, minted fresh for every dispatch, including a retry or a continuation of a resumed session. Used by cost_budget to match the running session’s recorded spend in session_metadata, and by notify_operator to read the matching session id (see below). |
SORTIE_SESSION_AGENT_KIND | string | Dispatch-frozen agent kind for the session (e.g., claude-code). Written unconditionally; may be empty when no agent kind is resolved. Consumed by the notify_operator envelope to record the agent that ran the session. |
SORTIE_ATTEMPT | string | Current retry attempt number as a decimal integer. Written when the orchestrator has attempt information (retries and continuations). Absent on the very first dispatch. Starts at 1 for the first retry and increments on subsequent retries. |
The agent’s session identifier is not one of these variables. Sortie writes the tool server’s environment before that identifier is known, so an environment variable would always read empty. Instead, the notify_operator tool reads the session id live, at call time, from a .sortie/dispatch.json record the worker keeps current for the running dispatch, matched against SORTIE_DISPATCH_ID. See notify_operator for how that record is populated and when the session id it reports is empty.
Per-session variables always win. A stale SORTIE_ISSUE_ID in the process environment is overwritten by the orchestrator’s value for the active issue.
Credential delivery
Tier 2 tools (like tracker_api) need tracker API credentials. These reach the MCP server through the env block: the worker’s process environment contains credential variables (e.g., SORTIE_JIRA_API_KEY referenced by tracker.api_key: $SORTIE_JIRA_API_KEY), the SORTIE_* prefix scan collects them, and the worker writes them into .sortie/mcp.json. The MCP server parses the workflow file with the same config loader the orchestrator uses, so its $VAR resolution (see $VAR indirection in WORKFLOW.md) expands references against these variables.
When the operator uses --env-file, the CLI exports the resolved absolute path as SORTIE_ENV_FILE in the process environment. The prefix scan captures this variable, so the MCP server receives the .env file path and applies the overrides in it through the same loader.
The .sortie/mcp.json file is written with 0o600 permissions (owner read/write only) and resides within the per-issue workspace directory. The credential is already available to the agent subprocess, which inherits the full process environment: writing it to the config file does not expand the agent’s access.
Controlled environment
Unlike the hook subprocess environment, which uses a POSIX allowlist plus SORTIE_* prefix filter on the parent process, the MCP server’s environment is the one the env block names. Where an adapter re-expresses the configuration rather than passing its path, a name in that block can be resolved against the agent runtime’s own process environment instead of against a value written into the configuration; see translated delivery. Either way the names come from the env block. Sortie writes no variable outside the SORTIE_* namespace into the configuration and asks for none by name, so a non-SORTIE_* variable of the orchestrator’s process (e.g., PATH, HOME, ANTHROPIC_API_KEY) is not one Sortie hands to the MCP server. The prefix acts as a bounded namespace: no non-Sortie secrets leak into the config file.
Translated delivery and the env block
An adapter that re-expresses the generated configuration rather than passing its path can deliver an env entry by name instead of by value. The codex adapter does: when its own process already holds a variable of that name under the same value, it renders the name into the runtime’s environment-passthrough key and writes no value, and the runtime resolves the value from the app-server’s process environment when it spawns the server. Every other entry is written out with its value.
The reason is the delivery route. That adapter’s configuration travels on the app-server’s command line, and a value written there would sit on an argument list any other user of the host can read. A credential Sortie already holds therefore travels as a name. See the Codex adapter reference for the rendering, and delivery by agent kind for which kinds translate.
Relationship to hook variables
Four per-session variables (SORTIE_ISSUE_ID, SORTIE_ISSUE_IDENTIFIER, SORTIE_WORKSPACE, SORTIE_ATTEMPT) are shared with the hook subprocess environment. SORTIE_DB_PATH, SORTIE_DISPATCH_ID, and SORTIE_SESSION_AGENT_KIND are specific to the MCP execution channel: hooks don’t receive them. In hooks, SORTIE_ATTEMPT is always present (defaulting to 0 on the first dispatch). In the MCP env block, SORTIE_ATTEMPT is written only when the orchestrator has attempt information (retries and continuations); on the very first dispatch it is absent from the per-session set, though it may still appear if the operator’s process environment contains a SORTIE_ATTEMPT variable captured by the SORTIE_* prefix scan.
Install script variables
The install.sh script accepts three environment variables that control installation behavior.
| Variable | Default | Description |
|---|---|---|
SORTIE_VERSION | Latest GitHub release | Pin a specific release tag (e.g., 1.19.0). When set, the script skips the GitHub API call to discover the latest version. |
SORTIE_INSTALL_DIR | /usr/local/bin (root) or ~/.local/bin (non-root) | Override the directory where the sortie binary is placed. |
SORTIE_NO_VERIFY | 0 | Set to 1 to skip SHA-256 checksum verification of the downloaded binary. |
Example:
SORTIE_VERSION=1.19.0 SORTIE_INSTALL_DIR=/opt/bin \
curl -sSL https://get.sortie-ai.com/install.sh | shSee also
- WORKFLOW.md configuration reference: all configuration fields, defaults, and types
- CLI reference: command-line flags (including
--env-file) and exit codes - Agent extensions reference: tool schemas, MCP execution channel, and response formats
- Prometheus metrics reference:
sortie_*metric names (these are Prometheus metrics, not environment variables)
Was this page helpful?