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). Deprecated |
SORTIE_TRACKER_COMMENTS_ON_COMPLETION | tracker.comments.on_completion | bool. Deprecated |
SORTIE_TRACKER_COMMENTS_ON_FAILURE | tracker.comments.on_failure | bool. Deprecated |
The three SORTIE_TRACKER_COMMENTS_* variables are deprecated with the keys they set. A value that resolves to true draws the same warning as the key, which names the tracker_comment replacement.
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 (sets the string form and replaces a list written in the file) |
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_COST_USD | agent.max_cost_usd | number |
SORTIE_AGENT_MAX_DAILY_COST_USD | agent.max_daily_cost_usd | number |
SORTIE_AGENT_MAX_WEEKLY_COST_USD | agent.max_weekly_cost_usd | number |
SORTIE_AGENT_MAX_MONTHLY_COST_USD | agent.max_monthly_cost_usd | number |
SORTIE_AGENT_TOKEN_WARNING_PERCENT | agent.token_warning_percent | int |
SORTIE_AGENT_MAX_CONSECUTIVE_ABSENCES | agent.max_consecutive_absences | int |
Price table variables
| Env var | Overrides | Type |
|---|---|---|
SORTIE_PRICE_TABLE_FETCH | price_table.fetch | bool (true/false/1/0) |
SORTIE_PRICE_TABLE_FETCH=false stops the download of the shared price table, which is how a network-restricted or audited deployment keeps the request off the wire. It does not stop the download of a table named by price_table.replace_url. No other price_table key has an override.
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) |
| number | Parsed as a decimal number, with leading and trailing whitespace trimmed first. | Startup error: config: agent.max_cost_usd: invalid number value: abc (from SORTIE_AGENT_MAX_COST_USD) |
| 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. |
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. tracker.kind,tracker.endpoint,tracker.api_key,tracker.project,tracker.query_filterandtracker.api_versionare read once, when the tracker adapter is built at startup. A changed value for one of them, also one set through.env, takes effect at the next restart. The dynamic reload table lists when every other field takes effect.
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 parent process environment, less the few variables an adapter manages itself, which its reference page names. Sortie validates none of these variables: they pass straight through, and if one is missing, the agent subprocess fails, not Sortie. Sortie reads four of them for itself, to locate the files a runtime writes under its home: CLAUDE_CONFIG_DIR, CODEX_HOME, COPILOT_HOME, and GEMINI_CLI_HOME. On a remote launch the remote shell resolves each one on the worker host.
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. |
CLAUDE_CODE_EFFORT_LEVEL | claude-code adapter (optional) | Sets Claude Code’s effort level. Ignored while claude-code.effort is set: Sortie then withholds it from every launch, local and remote. It keeps its effect while effort is unset. |
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 settles a turn the usage checkpoints on the event stream leave open. An empty or unset value resolves to the default. On a remote launch the remote shell resolves the variable on the worker host; see usage measurement over SSH. |
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. |
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 credential verification 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 Copilot CLI itself implements try-and-fallback across COPILOT_GITHUB_TOKEN, GH_TOKEN, and GITHUB_TOKEN; precedence matters only when multiple sources hold different valid tokens. Sortie itself reads the first of the three that is set in the agent’s environment to request the account’s usage limits when a turn ends.
Sortie runs no preflight of its own here: whether one of these variables resolves to a working credential is settled by the credential-verification step, which fails before any work starts when none of them does: with error kind credential_unverified, or with the early exit report under port_exit, carrying the CLI’s own message, when the CLI prints that message and exits.
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 usage-recovery subprocess. See the OpenCode adapter reference for what each carries.
| Variable | Purpose | Description |
|---|---|---|
OPENCODE_DISABLE_AUTOUPDATE | Self-update | Sortie-managed runs force this to true. |
OPENCODE_CONFIG_CONTENT | Inline configuration | Set on every turn, carrying the permission policy (from opencode.allowed_tools, opencode.denied_tools, and, when opencode.dangerously_skip_permissions is false, the external_directory setting), sharing (always "disabled"), compaction (from opencode.disable_autocompact), and the switch that disables the title agent. The document carries the tool servers on a local turn and on a remote turn alike; a remote turn that delivers them sets the variable on the SSH session’s standard input. |
On a local launch the adapter removes an inherited OPENCODE_AUTO_SHARE, OPENCODE_DISABLE_AUTOCOMPACT, OPENCODE_DISABLE_LSP_DOWNLOAD, or OPENCODE_PERMISSION so an operator-side value never reaches OpenCode, and it replaces an inherited OPENCODE_DISABLE_AUTOUPDATE and OPENCODE_CONFIG_CONTENT with its own.
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 OPENCODE_DISABLE_AUTOUPDATE and OPENCODE_CONFIG_CONTENT, plus the tool delivery variables when it delivers tools, and nothing else of Sortie’s own. The adapter strips nothing there and leaves the remote host’s own variables, OPENCODE_PERMISSION included, as they are. The provider credentials your model selection needs must already exist on the remote host, or be named under worker.ssh_pass_env.
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.
It still runs the credential-verification step like every other kind, and a runtime that answers its handshake with a protocol-level authentication error there fails the run with credential_unverified before any work starts.
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 |
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_DISABLE_AUTOUPDATE and OPENCODE_CONFIG_CONTENT 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, 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_RUN_OUTCOME | string | How the attempt ended. Values: "succeeded", "stopped" (the attempt needed input, was stopped at a budget ceiling or a provider usage limit, or ended with the agent declaring it is blocked), "failed", "cancelled" (for example, Sortie shut down during the attempt). Set on all after_run invocations, with or without --once, and it overrides a same-named variable of the parent environment. The value is decided before the handoff evidence check, so an attempt whose handoff the evidence policy then withholds still reads "succeeded". |
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 wherever its starter runs. In stdio mode it is a child process of the agent runtime, and the runtime builds its environment from the names in the env field of .sortie/mcp.json: a variable not listed in that block does not reach the server. In HTTP mode, used on an SSH launch and for a local agent-client-protocol runtime that accepts HTTP MCP servers, it is a child of the Sortie orchestrator and starts with Sortie’s own environment overlaid with that block. Either way the env block values win.
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.
Where it delivers nothing, no server starts and the env block reaches nobody; see delivery by agent kind. On an SSH launch the block never leaves the orchestrator host.
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) and to scope its notification cap to this dispatch. |
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, for a stdio server on a local launch: it adds the entry to the app-server’s own process environment and renders the name into the runtime’s environment-passthrough key, and the runtime resolves the value from that environment when it spawns the server. An entry stays a value in the configuration only when the app-server’s environment already holds that name with a different value, or when two stdio servers declare it with different values. Sortie then logs tool server env value left on the command line at WARN, naming the server and the variable and never the 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. See the Codex adapter reference for the rendering, and delivery by agent kind for which kinds translate.
Tool delivery variables
Sortie sets these in the environment of a runtime so that a configuration names a secret instead of holding it.
| Variable | Set for | Value |
|---|---|---|
SORTIE_MCP_HEADER_<n> | claude-code, copilot-cli, codex, opencode, on a local launch and on an SSH launch | The value of one HTTP header of a server from mcp_config. <n> counts from 1 in byte order of server name, then header name. The configuration refers to the variable in place of the value. |
SORTIE_TOOL_SERVER_BEARER | The sortie mcp-server --http process, and on an SSH launch the claude-code, copilot-cli, codex, and opencode runtimes | The per-session bearer token for Sortie’s HTTP tool server. The server refuses an HTTP request that does not present it. |
The agent-client-protocol kind sets neither variable on the runtime, because the protocol has no reference form: it sends header values and the bearer inside the session/new request. On an SSH launch the variables travel on the SSH session’s standard input, never in an argument. The bearer is masked in Sortie’s log, run history, and dashboard while its session lives.
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?