# Config File

The config file lives at `~/.anteroom/config.yaml`.

## Full Reference

```yaml
ai:
  base_url: "https://your-ai-endpoint/v1"    # Required
  api_key: "your-api-key"                     # Required (or use api_key_command)
  api_key_command: "vault read -field=key"    # Alternative: run a command to get the key
  model: "gpt-4"                              # Default model
  system_prompt: "You are a helpful assistant."
  verify_ssl: true                            # SSL cert verification (default: true)
  request_timeout: 120                        # API request timeout in seconds (default: 120, clamped 10–600)
  connect_timeout: 5                           # TCP connect timeout in seconds (default: 5, clamped 1–30)
  write_timeout: 30                            # Time to send request body in seconds (default: 30, clamped 5–120)
  pool_timeout: 10                             # Wait for free connection from pool in seconds (default: 10, clamped 1–60)
  first_token_timeout: 30                      # Max wait for first token after connect (default: 30, clamped 5–120)
  chunk_stall_timeout: 30                      # Max silence between chunks mid-stream in seconds (default: 30, clamped 10–600)
  retry_max_attempts: 3                        # Retries on transient errors; 0 disables (default: 3, clamped 0–10)
  retry_backoff_base: 1.0                      # Exponential backoff base in seconds (default: 1.0, clamped 0.1–30.0)
  temperature: null                             # Model temperature for randomness (0.0–2.0, null = provider default)
  top_p: null                                   # Nucleus sampling parameter (0.0–1.0, null = provider default)
  seed: null                                    # Random seed for deterministic output (null = provider default)
  provider: "openai"                            # AI backend: "openai", "anthropic", or "litellm" (100+ providers)
  max_output_tokens: 4096                       # Max generated tokens per response (default: 4096; required by some providers)
  allowed_domains: []                           # Egress domain allowlist (empty = no restriction)
  allowed_models: []                            # Model allowlist (empty = show all from provider)
  block_localhost_api: false                    # When true, reject localhost/127.0.0.1 as base_url
  litellm_bedrock_tools_confirmed: false        # Opt-in for Bedrock tool calling via LiteLLM
  model_family_aliases: {}                      # Map Claude family names to model IDs (e.g. {haiku: "claude-3-haiku-20240307"})

app:
  host: "127.0.0.1"      # Bind address
  port: 8080              # Server port
  data_dir: "~/.anteroom"   # Where DB + attachments live
  tls: false              # Set true for HTTPS with self-signed cert

rate_limit:
  max_requests: 120       # Max requests per window per IP (default: 120)
  window_seconds: 60      # Sliding window size in seconds (default: 60)
  exempt_paths:           # Paths exempt from rate limiting
    - /api/events         # SSE endpoint for real-time updates
  sse_retry_ms: 5000      # EventSource retry interval in milliseconds (default: 5000)

storage:
  retention_days: 0                    # Days to retain conversations; 0 = disabled (default: 0)
  retention_check_interval: 3600       # Seconds between retention policy checks (default: 3600 = 1 hour, clamped 300–86400)
  purge_attachments: true              # Delete attachment files when conversations are purged (default: true)
  purge_embeddings: true               # Delete vector embeddings when conversations are purged (default: true)
  encrypt_at_rest: false               # Enable database encryption via SQLCipher (default: false, requires sqlcipher3)
  encryption_kdf: "hkdf-sha256"        # Key derivation function for encryption (default: hkdf-sha256)

session:
  store: "memory"                      # "memory" or "sqlite" (default: memory)
  max_concurrent_sessions: 0           # 0 = unlimited (default: 0)
  idle_timeout: 1800                   # Seconds (default: 1800 = 30 minutes)
  absolute_timeout: 43200              # Seconds (default: 43200 = 12 hours)
  allowed_ips: []                      # CIDR or exact addresses; empty = allow all (default: empty)
  log_session_events: false            # Log session lifecycle events (default: false)

cli:
  theme: "midnight"                # CLI color theme: midnight | dawn | high-contrast | accessible (default: midnight)
  builtin_tools: true              # Enable built-in tools (default: true)
  max_tool_iterations: 50          # Max tool calls per response (default: 50)
  max_consecutive_text_only: 3     # Stop after N text-only responses with no tool calls; 0 to disable (default: 3)
  max_line_repeats: 5              # Stop if a single response repeats the same line N+ times; 0 to disable (default: 5)
  context_warn_tokens: 80000       # Token count at which context warning is shown (default: 80000)
  context_auto_compact_tokens: 100000  # Token count at which auto-compaction triggers (default: 100000)
  retry_delay: 5.0                 # Seconds between CLI auto-retry countdown ticks (default: 5.0, clamped 1–60)
  max_retries: 3                   # Max CLI auto-retry attempts for retryable errors (default: 3, clamped 0–10)
  esc_hint_delay: 8.0              # Seconds before showing "esc to cancel" hint (default: 8.0, clamped 0+)
  stall_display_threshold: 5.0     # Seconds of chunk silence before showing "stalled" indicator (default: 5.0, clamped 1+)
  stall_warning_threshold: 15.0    # Seconds before showing full stall warning (default: 15.0, clamped 1+)
  stall_throughput_threshold: 30.0 # Chars/sec below which "slow" indicator shows during streaming (default: 30.0)
  tool_output_max_chars: 2000      # Max chars per tool result for live agent calls (default: 2000, clamped 100+)
  tool_replay_max_chars: 2000      # Max chars per tool result on conversation resume/replay (default: 2000, clamped 100+)
  show_attribution_footer: true    # Show the "[ctx] N turns · ..." footer after each assistant turn (default: true, #923)
  file_reference_max_chars: 100000 # Max chars from @file references (default: 100000, clamped 1000+)
  model_context_window: 128000     # Model context window size for usage bar (default: 128000, clamped 1000+)
  background_suggest_seconds: 30   # Advisory threshold for background shell tasks (0 = disabled, default: 30)
  detach_suggest_seconds: 120      # Advisory threshold for detached subagents (0 = disabled, default: 120)
  update_check: true               # Check for newer versions on startup (default: true)
  update_check_command: ""         # Custom command returning latest version (replaces pip index)
  usage:
    week_days: 7                   # Days for "this week" rolling window (default: 7)
    month_days: 30                 # Days for "this month" rolling window (default: 30)
    model_costs: {}                # Per-model costs: {model: {input: rate, output: rate}} (default: empty)
    budgets:
      enabled: false               # Enable token budget enforcement (default: false)
      max_tokens_per_request: 0    # Single request limit; 0 = unlimited (default: 0)
      max_tokens_per_conversation: 0  # Conversation limit; 0 = unlimited (default: 0)
      max_tokens_per_day: 0        # Daily limit; 0 = unlimited (default: 0)
      warn_threshold_percent: 80   # Warn at this % of limit (default: 80)
      action_on_exceed: block      # "block" to deny or "warn" to allow (default: "block")

identity:
  user_id: "auto-generated-uuid"
  display_name: "Your Name"
  public_key: "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
  private_key: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"

shared_databases:
  - name: "team-shared"
    path: "~/shared/team.db"

mcp_servers:
  - name: "my-tools"
    transport: "stdio"
    command: "npx"
    args: ["-y", "@my-org/mcp-tools"]
    env:
      API_KEY: "${MY_API_KEY}"
    tools_include: []                  # fnmatch allowlist (empty = include all)
    tools_exclude: []                  # fnmatch blocklist

  - name: "remote-tools"
    transport: "sse"
    url: "https://mcp-server.example.com/sse"

mcp_tool_warning_threshold: 40        # Warn when total MCP tools exceed this (0 = disabled)

safety:
  enabled: true
  approval_mode: "ask_for_writes"    # auto | ask_for_dangerous | ask_for_writes | ask
  approval_timeout: 120
  read_only: false                   # If true, only READ-tier tools are available
  bash:
    enabled: true
  write_file:
    enabled: true
  allowed_tools: []                  # Tools that skip approval (always auto-approved)
  denied_tools: []                   # Tools that are hard-blocked (never execute)
  tool_tiers: {}                     # Per-tool tier overrides, e.g. {my_mcp_tool: "read"}
  custom_patterns: []
  sensitive_paths: []
  subagent:
    max_concurrent: 5
    max_total: 10
    max_depth: 3
    max_iterations: 15
    timeout: 120
    max_output_chars: 4000
    max_prompt_chars: 32000
  tool_rate_limit:
    max_calls_per_minute: 0               # Max tool calls per minute (0 = unlimited)
    max_calls_per_conversation: 0         # Max tool calls per conversation (0 = unlimited)
    max_consecutive_failures: 5           # Max consecutive failed tool calls
    action: "block"                       # "block" to deny, "warn" to allow + log
  prompt_injection:
    enabled: false                        # Enable prompt injection detection (default: false)
    action: "warn"                        # "block" | "warn" | "log" (default: warn)
    canary_length: 16                     # Bytes of randomness for canary tokens (default: 16)
    detect_encoding_attacks: true         # Detect base64, hex, unicode escaping (default: true)
    detect_instruction_override: true     # Detect instruction override patterns (default: true)
    heuristic_threshold: 0.7              # Minimum confidence to trigger action (default: 0.7)
    log_detections: true                  # Log detected attacks to security log (default: true)

embeddings:
  enabled: true
  model: "text-embedding-3-small"
  dimensions: 1536
  base_url: ""
  api_key: ""
  api_key_command: ""
  cache_dir: ""                        # Custom model cache directory; enables local_files_only when set

rag:
  enabled: true                        # Master toggle for RAG (default: true)
  max_chunks: 10                       # Maximum chunks to retrieve per query (default: 10)
  max_tokens: 2000                     # Token budget for RAG context (default: 2000)
  similarity_threshold: 0.5            # Maximum cosine distance; lower = stricter (default: 0.5)
  include_sources: true                # Search knowledge source chunks (default: true)
  include_conversations: true          # Search past conversation messages (default: true)
  exclude_current: true                # Exclude current conversation from results (default: true)
  retrieval_mode: "dense"              # "dense", "keyword", or "hybrid" (default: "dense")
  show_status: true                    # Show RAG status messages in CLI (default: true)

reranker:
  enabled: null                        # null = auto-detect (use if fastembed available)
  provider: "local"                    # "local" (fastembed TextCrossEncoder)
  model: "cross-encoder/ms-marco-MiniLM-L-6-v2"
  top_k: 5                            # Keep top-K chunks after reranking
  score_threshold: 0.0                 # Minimum relevance score (0 = no threshold)
  candidate_multiplier: 3             # Fetch top_k * multiplier candidates before reranking
  cache_dir: ""                        # Custom model cache directory; enables local_files_only when set

memory:
  promotion:
    default_review_state: "candidate"    # "candidate" or "pending_review" (default: "candidate")
    local_auto_approve: false             # When true, proposals skip review → active with auto_approved lineage (default: false)
    agent_proposals_enabled: true         # When false, proposer="agent" is rejected with 403 (default: true)
    max_lineage_entries: 50               # FIFO cap on decision-history entries per memory (default: 50)
    max_candidates_per_conversation: 10   # Per-conversation proposal cap; overflow → 429 (default: 10)
    max_reject_reason_chars: 500          # Server-side bound on rejection reason; router also caps at 2000 (default: 500)
  retention:
    enabled: false                        # Master switch; default off (no memory eviction regardless of other fields)
    max_age_days: null                    # Purge memories older than N days (by created_at). null disables the rule
    idle_days: null                       # Purge memories not recalled in N days (by last_recalled_at → created_at fallback). null disables
    min_age_days: 1                        # Grace floor for idle_days; protects freshly-approved memories (default: 1)
    purge_statuses: ["rejected"]          # Statuses eligible for status-based eviction (default: ["rejected"])
    respect_pins: true                    # Skip memories with metadata.pinned == true (default: true)

dlp:
  enabled: false                       # Set true to enable DLP scanning
  scan_output: true                    # Scan AI responses (default: true)
  scan_input: false                    # Scan user input (default: false)
  action: "redact"                     # "redact" | "block" | "warn" (default: redact)
  redaction_string: "[REDACTED]"
  log_detections: true                 # Log matches to security log (default: true)
  # patterns: []                        # Built-in patterns loaded by default
  custom_patterns: []                  # Add custom regex rules

output_filter:
  enabled: false                       # Set true to enable output filtering
  system_prompt_leak_detection: true   # Detect system prompt leaks via n-gram analysis (default: true)
  leak_threshold: 0.4                  # Minimum similarity threshold for leak detection (0.0-1.0, default: 0.4)
  action: "warn"                       # "warn" | "block" | "redact" (default: warn)
  redaction_string: "[FILTERED]"       # String to replace filtered content when action is "redact"
  log_detections: true                 # Log filter detections to security log (default: true)
  custom_patterns: []                  # Add custom forbidden pattern rules

workflow:
  budget:
    max_duration_seconds: 0   # Wall-clock ceiling per run (0 = unlimited)
    max_steps: 0              # Max completed steps per run (0 = unlimited)
    max_tokens: 0             # Max total tokens per run (0 = unlimited)

# Project/team config only — shared references
references:
  instructions:
    - "team/instructions.md"
    - "team/coding-standards.md"
  rules:
    - "team/rules/no-eval.md"
  skills:
    - "team/skills/deploy"            # directory containing SKILL.md
    - "team/skills/review/SKILL.md"   # or explicit path to SKILL.md

# Project/team config only — required keys
required:
  - path: "ai.api_key"
    description: "Your API key"
  - path: "custom.db_password"
    description: "Database password"
```

## Sections

### ai

| Field | Type | Default | Description |
|---|---|---|---|
| `base_url` | string | --- | OpenAI-compatible API endpoint (required) |
| `api_key` | string | --- | API key (required unless using `api_key_command`) |
| `api_key_command` | string | --- | External command to obtain API key dynamically |
| `model` | string | `gpt-4` | Default model name |
| `system_prompt` | string | `You are a helpful assistant.` | System prompt for all conversations |
| `verify_ssl` | boolean | `true` | Verify SSL certificates when connecting to the API |
| `request_timeout` | integer | `120` | Overall stream timeout in seconds (clamped 10–600); env: `AI_CHAT_REQUEST_TIMEOUT` |
| `connect_timeout` | integer | `5` | TCP connect timeout in seconds (clamped 1–30); env: `AI_CHAT_CONNECT_TIMEOUT` |
| `write_timeout` | integer | `30` | Time to send request body in seconds (clamped 5–120); env: `AI_CHAT_WRITE_TIMEOUT` |
| `pool_timeout` | integer | `10` | Wait for free connection from pool in seconds (clamped 1–60); env: `AI_CHAT_POOL_TIMEOUT` |
| `first_token_timeout` | integer | `30` | Max seconds to wait for first token after connect (clamped 5–120); env: `AI_CHAT_FIRST_TOKEN_TIMEOUT` |
| `chunk_stall_timeout` | integer | `30` | Max silence between chunks mid-stream in seconds (clamped 10–600); env: `AI_CHAT_CHUNK_STALL_TIMEOUT` |
| `retry_max_attempts` | integer | `3` | Retries on transient errors (timeout, connection); 0 disables (clamped 0–10); env: `AI_CHAT_RETRY_MAX_ATTEMPTS` |
| `retry_backoff_base` | float | `1.0` | Exponential backoff base delay in seconds (clamped 0.1–30.0); env: `AI_CHAT_RETRY_BACKOFF_BASE` |
| `temperature` | float or null | `null` | Model temperature for response randomness (0.0–2.0; null = provider default); env: `AI_CHAT_TEMPERATURE` |
| `top_p` | float or null | `null` | Nucleus sampling parameter (0.0–1.0; null = provider default); env: `AI_CHAT_TOP_P` |
| `seed` | integer or null | `null` | Random seed for deterministic output (null = provider default); env: `AI_CHAT_SEED` |
| `provider` | string | `openai` | AI backend to use: `openai` (default, works with any OpenAI-compatible API), `anthropic` (native Anthropic Messages API, requires `pip install anteroom[anthropic]`), or `litellm` (100+ providers via LiteLLM — OpenRouter, Replicate, Together, Cohere, etc., requires `pip install anteroom[providers]`); env: `AI_CHAT_PROVIDER` |
| `max_output_tokens` | integer | `4096` | Maximum tokens to generate per response. Required by some providers (e.g. Anthropic). Ignored when the provider does not support the parameter; env: `AI_CHAT_MAX_OUTPUT_TOKENS` |
| `allowed_domains` | list[string] | `[]` | Egress domain allowlist for API requests (empty list = no restriction). Domains are matched case-insensitively as exact matches. Fails closed: unparseable URLs are rejected; env: `AI_CHAT_ALLOWED_DOMAINS` (comma-separated) |
| `allowed_models` | list[string] | `[]` | Model allowlist for the model selector and `/model` command (empty list = show all models from provider). When set, only listed models appear in the CLI picker and web UI dropdown. Enforced on model switch, conversation PATCH, and at chat runtime (defense in depth); env: `AI_CHAT_ALLOWED_MODELS` (comma-separated) |
| `block_localhost_api` | boolean | `false` | When true, reject localhost/127.0.0.1/[::1] as the API base_url. Useful in enterprise environments to prevent accidental connections to local services; env: `AI_CHAT_BLOCK_LOCALHOST_API` |
| `litellm_bedrock_tools_confirmed` | boolean | `false` | Opt-in for Bedrock tool calling via LiteLLM. When false (default), Anteroom blocks tool-calling requests routed through Bedrock because LiteLLM's internal toolConfig translation is known to fail on some Bedrock model versions. Set to true after verifying the route works end-to-end; env: `AI_CHAT_LITELLM_BEDROCK_TOOLS_CONFIRMED` |
| `model_family_aliases` | dict[string, string] | `{}` | Map model-family names to full model IDs for `run_agent` calls. Example: `{haiku: "claude-3-haiku-20240307", sonnet: "claude-sonnet-4-20250514"}`. Known Claude family names (`haiku`, `sonnet`, `opus`) that are not in the alias table are rejected locally with an actionable error. All other model values pass through unchanged; env: `AI_CHAT_MODEL_FAMILY_ALIASES` (JSON string, e.g. `'{"haiku":"claude-3-haiku-20240307"}'`) |

### app

| Field | Type | Default | Description |
|---|---|---|---|
| `host` | string | `127.0.0.1` | Bind address for the web server |
| `port` | integer | `8080` | Port for the web server; env: `AI_CHAT_PORT` |
| `data_dir` | string | `~/.anteroom` | Directory for database, attachments, config |
| `tls` | boolean | `false` | Enable HTTPS with self-signed certificate |

### rate_limit

HTTP request rate limiting to prevent abuse and SSE storms. Protects against EventSource reconnection floods.

| Field | Type | Default | Description |
|---|---|---|---|
| `max_requests` | integer | `120` | Maximum requests per IP address per time window (sliding window); env: `AI_CHAT_RATE_LIMIT_MAX_REQUESTS` |
| `window_seconds` | integer | `60` | Sliding window size in seconds; env: `AI_CHAT_RATE_LIMIT_WINDOW_SECONDS` |
| `exempt_paths` | list[string] | `["/api/events"]` | URL paths exempt from rate limiting (e.g., SSE endpoints); env: `AI_CHAT_RATE_LIMIT_EXEMPT_PATHS` (comma-separated) |
| `sse_retry_ms` | integer | `5000` | EventSource `retry:` field sent to browser clients (milliseconds). Controls reconnection backoff on 429 responses; env: `AI_CHAT_RATE_LIMIT_SSE_RETRY_MS` |

Example:

```yaml
rate_limit:
  max_requests: 120
  window_seconds: 60
  exempt_paths:
    - /api/events
    - /health
  sse_retry_ms: 5000
```

When a client exceeds the rate limit, the server returns HTTP 429 (Too Many Requests) with the `Retry-After` header. For SSE connections, the server sends a `retry:` field directing the EventSource to wait before reconnecting. Paths in `exempt_paths` are not rate-limited, allowing critical endpoints like `/api/events` to handle high-frequency client reconnections.

### storage

Controls data retention policies and encryption at rest for the SQLite database.

| Field | Type | Default | Description |
|---|---|---|---|
| `retention_days` | integer | `0` | Number of days to retain conversations before automatic purge; `0` = disabled (retention disabled); env: `AI_CHAT_RETENTION_DAYS` |
| `retention_check_interval` | integer | `3600` | Seconds between retention policy checks (clamped 300–86400); env: `AI_CHAT_RETENTION_CHECK_INTERVAL` |
| `purge_attachments` | boolean | `true` | Delete attachment files from disk when conversations are purged (default: true); env: `AI_CHAT_PURGE_ATTACHMENTS` |
| `purge_embeddings` | boolean | `true` | Delete vector embeddings from the database when conversations are purged (default: true); env: `AI_CHAT_PURGE_EMBEDDINGS` |
| `encrypt_at_rest` | boolean | `false` | Enable database encryption via SQLCipher. Requires sqlcipher3: `pip install anteroom[sqlcipher]` (default: false); env: `AI_CHAT_ENCRYPT_AT_REST` |
| `encryption_kdf` | string | `hkdf-sha256` | Key derivation function for encryption (default: `hkdf-sha256`); env: `AI_CHAT_ENCRYPTION_KDF` |

#### Data Retention

When `retention_days` is set to a positive value, the retention worker automatically purges conversations (and all associated messages, tool_calls, and embeddings) older than the configured period. The retention check runs at the interval specified by `retention_check_interval`.

To manually purge conversations, use:

```bash
aroom db purge --older-than 30d      # purge conversations older than 30 days
aroom db purge --before 2025-01-01   # purge conversations before a specific date
aroom db purge --dry-run             # show what would be deleted
```

#### Encryption at Rest

When `encrypt_at_rest` is enabled:

- The database is encrypted using SQLCipher with a 256-bit key
- The key is derived from your Ed25519 identity key via HKDF-SHA256
- All database queries go through the encrypted connection transparently
- Attachments are NOT encrypted (only the database is)
- Requires `sqlcipher3` package: `pip install anteroom[sqlcipher]`

To initialize encryption on an existing database:

```bash
aroom db encrypt            # interactive setup
aroom db encrypt --key-from identity  # use identity key as encryption key
```

!!! note "One-way operation"
    Encryption is not reversible. Once enabled on a database, it cannot be decrypted without the original identity key. Back up your config before enabling.

### session

| Field | Type | Default | Description |
|---|---|---|---|
| `store` | string | `memory` | Session storage backend: `memory` (volatile, in-process) or `sqlite` (persistent); env: `AI_CHAT_SESSION_STORE` |
| `max_concurrent_sessions` | integer | `0` | Max concurrent sessions per user; `0` = unlimited. Enforced at middleware. When exceeded, returns 429 Too Many Sessions; env: `AI_CHAT_SESSION_MAX_CONCURRENT` |
| `idle_timeout` | integer | `1800` | Session idle timeout in seconds (default: 1800 = 30 minutes). Automatically cleaned up on next request; env: `AI_CHAT_SESSION_IDLE_TIMEOUT` |
| `absolute_timeout` | integer | `43200` | Absolute session lifetime in seconds (default: 43200 = 12 hours). After this period, session is invalidated regardless of activity; env: `AI_CHAT_SESSION_ABSOLUTE_TIMEOUT` |
| `allowed_ips` | list[string] | `[]` | IP allowlist for access control. Supports exact addresses (`192.168.1.5`) and CIDR ranges (`10.0.0.0/8`). Both IPv4 and IPv6. Empty list allows all IPs; env: `AI_CHAT_SESSION_ALLOWED_IPS` (comma-separated) |
| `log_session_events` | boolean | `false` | Log session lifecycle events (create, touch, delete) to audit log; env: `AI_CHAT_SESSION_LOG_EVENTS` |

### cli

| Field | Type | Default | Description |
|---|---|---|---|
| `builtin_tools` | boolean | `true` | Enable the 6 built-in tools |
| `max_tool_iterations` | integer | `50` | Max agentic loop iterations per turn |
| `max_consecutive_text_only` | integer | `3` | Stop after N consecutive text-only responses with no tool calls; 0 to disable |
| `max_line_repeats` | integer | `5` | Stop if a single response repeats the same line N+ times; 0 to disable |
| `context_warn_tokens` | integer | `80000` | Token count at which a context warning is shown in the CLI |
| `context_auto_compact_tokens` | integer | `100000` | Token count at which context is automatically compacted |
| `retry_delay` | float | `5.0` | Seconds between CLI auto-retry countdown ticks (clamped 1–60) |
| `max_retries` | integer | `3` | Max CLI auto-retry attempts for retryable errors (clamped 0–10) |
| `esc_hint_delay` | float | `8.0` | Seconds before showing "esc to cancel" hint |
| `stall_display_threshold` | float | `5.0` | Seconds of chunk silence before showing "stalled" indicator (clamped 1+) |
| `stall_warning_threshold` | float | `15.0` | Seconds before showing full stall warning (clamped 1+) |
| `stall_throughput_threshold` | float | `30.0` | Chars/sec below which "slow" indicator shows during streaming |
| `tool_output_max_chars` | integer | `2000` | Max chars per tool result for live agent calls (applied as the tool result is appended to the conversation); clamped 100+; env: `AI_CHAT_TOOL_OUTPUT_MAX_CHARS` |
| `tool_replay_max_chars` | integer | `2000` | Max chars per tool result when replaying historical tool results on conversation resume; clamped 100+; env: `AI_CHAT_TOOL_REPLAY_MAX_CHARS` |
| `show_attribution_footer` | boolean | `true` | Show the `[ctx] N turns · N memories · …` footer after each assistant turn. The full snapshot is always persisted to `messages.metadata["attribution"]` and stays reachable via `/attribution`; this setting only controls the footer print. Env override: `AI_CHAT_SHOW_ATTRIBUTION_FOOTER` (`false`/`0`/`no` disables). See [Context Attribution](../knowledge/attribution.md) ([#923](https://github.com/troylar/anteroom/issues/923)) |
| `file_reference_max_chars` | integer | `100000` | Max chars from @file references (clamped 1000+) |
| `model_context_window` | integer | `128000` | Model context window size for usage bar (clamped 1000+) |
| `background_suggest_seconds` | integer | `30` | Advisory threshold: suggest `background: true` for shell commands above this duration (0 = disabled) |
| `detach_suggest_seconds` | integer | `120` | Advisory threshold: suggest `detach: true` for AI work above this duration (0 = disabled) |
| `update_check` | boolean | `true` | Check PyPI for newer versions on CLI startup. Set to `false` to disable entirely (no subprocess spawned); env: `AI_CHAT_UPDATE_CHECK` |
| `update_check_command` | string | `""` | Custom shell command that prints the latest version to stdout (e.g., `curl -s https://internal.corp/anteroom/latest`). Replaces the default `pip index versions` check. Runs with a 5s timeout; env: `AI_CHAT_UPDATE_CHECK_COMMAND` |

### identity

User identity for message attribution in shared databases. Auto-generated on first run via `aroom init` or on startup if missing.

| Field | Type | Description |
|---|---|---|
| `user_id` | string | UUID identifying this user (auto-generated, immutable) |
| `display_name` | string | Human-readable name shown on messages |
| `public_key` | string | Ed25519 public key in PEM format (auto-generated) |
| `private_key` | string | Ed25519 private key in PEM format (auto-generated) |

!!! warning "Back up your identity"
    The `identity` section contains a private key that proves ownership of your user ID. If you lose it, your messages in shared databases become unverifiable. Back up your `config.yaml` to preserve your identity across reinstalls.

### shared_databases

A list of additional SQLite databases. See [Shared Databases](../web-ui/shared-databases.md).

| Field | Type | Description |
|---|---|---|
| `name` | string | Display name (alphanumeric, hyphens, underscores) |
| `path` | string | Path to `.db`/`.sqlite`/`.sqlite3` file |

### mcp_servers

A list of MCP tool servers. See [MCP Servers](mcp-servers.md).

Per-server configuration:

| Field | Type | Default | Description |
|---|---|---|---|
| `name` | string | --- | Display name for the server |
| `transport` | string | --- | Transport type: `"stdio"` (local process) or `"sse"` (remote HTTP) |
| `command` | string | --- | (stdio only) Command to launch the MCP server process |
| `args` | list | `[]` | (stdio only) Command arguments |
| `url` | string | --- | (SSE only) Server-Sent Events endpoint URL |
| `env` | map | `{}` | (stdio only) Environment variables for the process; supports `${VAR}` expansion |
| `timeout` | float | `30.0` | Connection timeout in seconds |
| `tools_include` | list | `[]` | Fnmatch patterns for tools to include (empty = include all) |
| `tools_exclude` | list | `[]` | Fnmatch patterns for tools to exclude |

### mcp_tool_warning_threshold

| Field | Type | Default | Description |
|---|---|---|---|
| `mcp_tool_warning_threshold` | integer | `40` | Emit a warning when total MCP tools across all servers exceed this threshold (0 = disabled) |

### trusted_proxy

Controls client IP resolution when Anteroom runs behind a reverse proxy (nginx, Caddy, HAProxy, etc.). Disabled by default — the TCP socket peer address is used as-is. When enabled, the configured forwarded header is parsed right-to-left; the first IP not in `trusted_cidrs` is the real client. If every IP in the chain is trusted (or the chain is empty or malformed), the resolver falls back to the socket peer (fail closed). See [Client IP Resolution](../security/authentication.md#client-ip-resolution-trusted-proxy) for the full algorithm.

| Field | Type | Default | Description | Env var |
|---|---|---|---|---|
| `enabled` | boolean | `false` | Enable trusted-proxy header parsing. When `false`, socket peer IP is always used | `AI_CHAT_TRUSTED_PROXY_ENABLED` |
| `trusted_cidrs` | list[string] | `[]` | CIDR ranges (IPv4 or IPv6) of trusted proxies, e.g. `["10.0.0.0/8", "172.16.0.0/12"]`. Both the socket peer and any intermediate hops must fall within these ranges to be considered trusted | `AI_CHAT_TRUSTED_PROXY_CIDRS` (comma-separated) |
| `header` | string | `X-Forwarded-For` | Header to parse for the forwarded client IP. Common alternatives: `CF-Connecting-IP` (Cloudflare), `X-Real-IP` (nginx single-hop) | `AI_CHAT_TRUSTED_PROXY_HEADER` |

Example — nginx or Caddy deployment on an internal network:

```yaml
trusted_proxy:
  enabled: true
  trusted_cidrs:
    - "10.0.0.0/8"        # internal network
    - "172.16.0.0/12"     # Docker bridge networks
    - "127.0.0.1/32"      # localhost (same-host proxy)
  header: "X-Forwarded-For"
```

### safety

Controls the tool safety approval gate. Tools are assigned risk tiers (read, write, execute, destructive) and the approval mode determines which tiers require user confirmation before execution.

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | boolean | `true` | Enable the safety gate globally |
| `approval_mode` | string | `ask_for_writes` | Which tiers require approval: `auto` (none), `ask_for_dangerous` (destructive only), `ask_for_writes` (write+execute+destructive), `ask` (same as ask_for_writes) |
| `approval_timeout` | integer | `120` | Seconds to wait for approval before blocking the operation (clamped 10–600) |
| `read_only` | boolean | `false` | If true, only READ-tier tools are available; all WRITE, EXECUTE, and DESTRUCTIVE tools are blocked; env: `AI_CHAT_READ_ONLY` |
| `bash.enabled` | boolean | `true` | Enable `bash` tool (set false to hard-block) |
| `write_file.enabled` | boolean | `true` | Enable `write_file` tool (set false to hard-block) |
| `allowed_tools` | list | `[]` | Tools that always skip approval regardless of tier |
| `denied_tools` | list | `[]` | Tools that are hard-blocked and never execute |
| `tool_tiers` | dict | `{}` | Per-tool tier overrides, e.g. `{my_mcp_tool: "read"}`. Valid tiers: `read`, `write`, `execute`, `destructive` |
| `custom_patterns` | list | `[]` | Additional regex patterns that trigger confirmation for bash commands |
| `sensitive_paths` | list | `[]` | Additional path prefixes that trigger confirmation for file writes |
| `subagent.*` | object | see below | Sub-agent execution limits (nested under `safety.subagent`) |

For bash-specific controls (`safety.bash.*`) and OS-level resource limits (`safety.bash.sandbox.*`), see [Sandboxing Overview](../security/sandboxing.md) and [Bash Sandboxing](../security/bash-sandboxing.md).

#### safety.subagent

Controls limits for the `run_agent` sub-agent tool. All fields are optional — sensible defaults apply when omitted.

| Field | Type | Default | Description |
|---|---|---|---|
| `max_concurrent` | integer | `5` | Maximum sub-agents running simultaneously |
| `max_total` | integer | `10` | Maximum sub-agents spawned per root request |
| `max_depth` | integer | `3` | Maximum nesting depth (sub-agents spawning sub-agents) |
| `max_iterations` | integer | `15` | Maximum agentic loop iterations per sub-agent |
| `timeout` | integer | `120` | Wall-clock timeout in seconds per sub-agent (clamped 10–600) |
| `max_output_chars` | integer | `4000` | Maximum output characters returned to parent |
| `max_prompt_chars` | integer | `32000` | Maximum prompt characters accepted |

#### safety.tool_rate_limit

Controls tool call rate limiting to prevent tool abuse and excessive iterations. All limits default to 0 (unlimited). Set a positive value to enable.

| Field | Type | Default | Description |
|---|---|---|---|
| `max_calls_per_minute` | integer | `0` | Maximum tool calls allowed per minute (0 = unlimited); applies globally across all conversations |
| `max_calls_per_conversation` | integer | `0` | Maximum tool calls allowed per conversation thread (0 = unlimited). Counts accumulated calls from root and all sub-agents |
| `max_consecutive_failures` | integer | `5` | Maximum consecutive tool failures before rate limit triggers. Useful to break infinite error loops |
| `action` | string | `block` | Action when rate limit is exceeded: `block` (deny request with error) or `warn` (log warning, allow execution) |

Example configuration to prevent runaway agents:

```yaml
safety:
  tool_rate_limit:
    max_calls_per_minute: 30              # max 30 tool calls per minute
    max_calls_per_conversation: 100       # max 100 total calls per conversation
    max_consecutive_failures: 3           # block after 3 consecutive failures
    action: "block"                       # hard block when limits exceeded
```

See [Tool Safety](../security/tool-safety.md) for the full list of built-in patterns and the approval flow.

#### safety.prompt_injection

Controls prompt injection detection and defense. Detects when untrusted content (tool outputs, RAG results) attempts to override system instructions through canary token leakage, encoding attacks, and heuristic pattern matching.

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | boolean | `false` | Enable prompt injection detection |
| `action` | string | `warn` | Action when injection is detected: `block` (reject content), `warn` (allow + log warning), `log` (silent audit) |
| `canary_length` | integer | `16` | Length in bytes of random canary tokens embedded in system prompt for leakage detection |
| `detect_encoding_attacks` | boolean | `true` | Detect obfuscated injection attempts (base64, hex, unicode escaping) |
| `detect_instruction_override` | boolean | `true` | Detect patterns that attempt to override or ignore system instructions |
| `heuristic_threshold` | float | `0.7` | Confidence threshold (0.0–1.0) for heuristic pattern matching; higher = fewer false positives |
| `log_detections` | boolean | `true` | Log all detected injections to the security logger |

Example to enable strict injection defense:

```yaml
safety:
  prompt_injection:
    enabled: true
    action: "block"                       # hard-block injections
    detect_encoding_attacks: true
    detect_instruction_override: true
    heuristic_threshold: 0.6              # lower threshold catches more attempts
    log_detections: true
```

See [Prompt Injection Defense](../security/prompt-injection-defense.md) for technical details and threat model.

### embeddings

Controls vector embeddings for semantic search. Requires an OpenAI-compatible embedding endpoint and the sqlite-vec extension.

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | boolean | `true` | Enable background embedding of messages |
| `model` | string | `text-embedding-3-small` | Embedding model name |
| `dimensions` | integer | `1536` | Vector dimensions (must match the model) |
| `base_url` | string | `""` | Embedding API endpoint (falls back to `ai.base_url` if empty) |
| `api_key` | string | `""` | API key for the embedding endpoint |
| `api_key_command` | string | `""` | External command to obtain the embedding API key dynamically |
| `cache_dir` | string | `""` | Custom fastembed model cache directory; when set, enables `local_files_only` mode to prevent network requests; env: `AI_CHAT_EMBEDDINGS_CACHE_DIR` |

### compaction

Shared configuration for conversation history compaction.  Owned by the compaction service (`services/compaction.py`).  Both the shared agent loop's auto-compact path and the CLI `/compact` command read from this section.

| Field | Type | Default | Description |
|---|---|---|---|
| `preserve_tail` | integer | `6` | Recent messages preserved intact during boundary-based compaction; `0` reverts to full-summary behaviour.  Clamped to `[0, 200]` to guard against misconfiguration silently defeating compaction.  Env: `AI_CHAT_COMPACT_PRESERVE_TAIL` |

The legacy `cli.compact_preserve_tail` key is accepted as a deprecated alias when `compaction.preserve_tail` is not set.

### rag

Controls the RAG retrieval pipeline --- what gets searched, how many results, filtering thresholds, and retrieval strategy.

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | boolean | `true` | Master toggle; `false` disables RAG entirely; env: `AI_CHAT_RAG_ENABLED` |
| `max_chunks` | integer | `10` | Maximum chunks to retrieve per query; env: `AI_CHAT_RAG_MAX_CHUNKS` |
| `max_tokens` | integer | `2000` | Token budget for injected RAG context (estimated as chars / 4); env: `AI_CHAT_RAG_MAX_TOKENS` |
| `similarity_threshold` | float | `0.5` | Maximum cosine distance; only applies in `dense` mode; env: `AI_CHAT_RAG_SIMILARITY_THRESHOLD` |
| `include_sources` | boolean | `true` | Search knowledge source chunks |
| `include_conversations` | boolean | `true` | Search past conversation messages |
| `exclude_current` | boolean | `true` | Exclude current conversation from message search |
| `retrieval_mode` | string | `dense` | Retrieval strategy: `dense` (vector similarity), `keyword` (FTS5), or `hybrid` (both via RRF); env: `AI_CHAT_RAG_RETRIEVAL_MODE` |
| `show_status` | boolean | `true` | Show RAG status messages (knowledge search notices) in CLI output; env: `AI_CHAT_RAG_SHOW_STATUS` |

### reranker

Controls cross-encoder reranking of RAG results. When enabled, retrieved chunks are re-scored by a cross-encoder model for improved relevance before being injected into the prompt. Uses fastembed `TextCrossEncoder` locally by default (no external API needed).

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | boolean | `null` (auto-detect) | Enable reranking; `null` uses reranking when fastembed is available; env: `AI_CHAT_RERANKER_ENABLED` |
| `provider` | string | `local` | Provider: `local` (fastembed TextCrossEncoder); only local is supported; env: `AI_CHAT_RERANKER_PROVIDER` |
| `model` | string | `cross-encoder/ms-marco-MiniLM-L-6-v2` | Cross-encoder model name; env: `AI_CHAT_RERANKER_MODEL` |
| `top_k` | integer | `5` | Keep top-K chunks after reranking (capped to `rag.max_chunks` at runtime); env: `AI_CHAT_RERANKER_TOP_K` |
| `score_threshold` | float | `0.0` | Minimum relevance score; cross-encoder logits can be negative; env: `AI_CHAT_RERANKER_SCORE_THRESHOLD` |
| `candidate_multiplier` | integer | `3` | Fetch `top_k * candidate_multiplier` candidates before reranking; env: `AI_CHAT_RERANKER_CANDIDATE_MULTIPLIER` |
| `cache_dir` | string | `""` | Custom fastembed model cache directory; when set, enables `local_files_only` mode to prevent network requests; env: `AI_CHAT_RERANKER_CACHE_DIR` |

### dlp

Controls Data Loss Prevention scanning for sensitive patterns (SSN, credit card, email, phone, IBAN). See [DLP documentation](../security/dlp.md) for detailed configuration and use cases.

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | boolean | `false` | Enable DLP scanning |
| `scan_output` | boolean | `true` | Scan AI responses for sensitive data |
| `scan_input` | boolean | `false` | Scan user input for sensitive data |
| `action` | string | `redact` | Action on match: `redact` (replace with `redaction_string`), `block` (reject), or `warn` (allow + log) |
| `redaction_string` | string | `[REDACTED]` | String to replace matched patterns when action is `redact` |
| `log_detections` | boolean | `true` | Log all DLP detections to the security logger |
| `patterns` | list | `[]` | Built-in patterns are loaded automatically (SSN, credit card, email, phone, IBAN) |
| `custom_patterns` | list | `[]` | Custom regex patterns. Each is a dict with `name`, `pattern` (regex), and `description` |

### output_filter

Controls output content filtering to detect and prevent system prompt leaks and forbidden content in LLM responses. See [Output Filter documentation](../security/output-filter.md) for detailed configuration and use cases.

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | boolean | `false` | Enable output content filtering |
| `system_prompt_leak_detection` | boolean | `true` | Detect system prompt leaks via n-gram analysis of LLM output against the system prompt |
| `leak_threshold` | float | `0.4` | Minimum n-gram similarity threshold (0.0–1.0) for flagging a potential leak |
| `action` | string | `warn` | Action on filter match: `warn` (allow + log), `redact` (replace with `redaction_string`), or `block` (reject) |
| `redaction_string` | string | `[FILTERED]` | String to replace filtered content when action is `redact` |
| `log_detections` | boolean | `true` | Log all filter detections to the security logger |
| `custom_patterns` | list | `[]` | Custom regex patterns for forbidden content. Each is a dict with `name`, `pattern` (regex), and `description` |

### usage

Controls token usage tracking and cost estimation for the CLI. All fields are optional with sensible defaults.

| Field | Type | Default | Description |
|---|---|---|---|
| `week_days` | integer | `7` | Number of days to include in "this week" rolling window |
| `month_days` | integer | `30` | Number of days to include in "this month" rolling window |
| `model_costs` | dict | `{}` | Model pricing for cost estimation, keyed by model name with `input` and `output` rates (per-million-token) |

Example with cost estimation:

```yaml
cli:
  usage:
    week_days: 7
    month_days: 30
    model_costs:
      gpt-4o: { input: 0.003, output: 0.006 }
      gpt-4-turbo: { input: 0.01, output: 0.03 }
      claude-3-sonnet: { input: 0.003, output: 0.015 }
```

The `/usage` command displays token counts and estimated costs across multiple time periods. Without `model_costs` configured, only token counts are shown.

#### usage.budgets

Nested under `usage` — controls token budget enforcement for denial-of-wallet prevention.

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | boolean | `false` | Enable token budget enforcement |
| `max_tokens_per_request` | integer | `0` | Maximum tokens for a single request (0 = unlimited) |
| `max_tokens_per_conversation` | integer | `0` | Maximum tokens accumulated in one conversation (0 = unlimited) |
| `max_tokens_per_day` | integer | `0` | Maximum tokens consumed in a calendar day (0 = unlimited) |
| `warn_threshold_percent` | integer | `80` | Warn when usage exceeds this percentage of any limit (0-100) |
| `action_on_exceed` | string | `block` | What to do when a limit is exceeded: `block` (deny request) or `warn` (allow but notify) |

Example:

```yaml
cli:
  usage:
    model_costs:
      gpt-4o: { input: 0.003, output: 0.006 }
    budgets:
      enabled: true
      max_tokens_per_request: 100000     # single request cap
      max_tokens_per_conversation: 500000 # conversation cap
      max_tokens_per_day: 2000000         # daily cap
      warn_threshold_percent: 80          # warn at 80%
      action_on_exceed: block             # hard block when exceeded
```

Budgets are checked at the start of each request. When a limit is exceeded with `action_on_exceed: block`, the request is rejected with a `budget_exceeded` error. With `action_on_exceed: warn`, the request proceeds but a `budget_warning` event is emitted.

### workflow

Controls resource limits, credential definitions, and behavior for the workflow engine.

```yaml
workflow:
  budget:
    max_duration_seconds: 0
    max_steps: 0
    max_tokens: 0
  credentials: []
  watch_buffer_lines: 50
```

#### workflow general

| Field | Type | Default | Env Var | Description |
|---|---|---|---|---|
| `workflow.watch_buffer_lines` | int | `50` | — | Maximum number of transcript lines to display in `workflow watch`. Older lines are dropped when the buffer is full |
| `workflow.lock_reclaim_threshold` | int | `120` | — | Seconds after which a run with a stale heartbeat has its lock reclaimed. Minimum 30 |

#### workflow.budget

| Field | Type | Default | Env Var | Description |
|---|---|---|---|---|
| `workflow.budget.max_duration_seconds` | int | `0` | — | Wall-clock ceiling in seconds. 0 = unlimited |
| `workflow.budget.max_steps` | int | `0` | — | Maximum step completions per run. 0 = unlimited |
| `workflow.budget.max_tokens` | int | `0` | — | Maximum total tokens (prompt + completion) per run. 0 = unlimited |

!!! note "Config budgets are ceilings"
    Config-level budgets act as ceilings — if a workflow definition also declares a budget, the effective limit is the minimum of non-zero values per dimension.

#### workflow.transcript

Controls durable transcript recording for workflow steps. Transcript events capture agent turns, tool calls, and runner output inside each step and are stored in the `workflow_events` table.

Transcript recording is **enabled by default**. Disable it by setting `enabled: false`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `workflow.transcript.enabled` | bool | `true` | Enable durable transcript recording for workflow steps |
| `workflow.transcript.max_assistant_chars` | int | `4000` | Max characters per assistant turn transcript entry |
| `workflow.transcript.max_tool_output_chars` | int | `2000` | Max characters per tool result transcript entry |
| `workflow.transcript.max_stdout_chars` | int | `4000` | Max characters for stdout transcript entries |
| `workflow.transcript.max_stderr_chars` | int | `4000` | Max characters for stderr transcript entries |

Content is truncated to the configured limit before storage. Truncation is applied per entry.

Example:

```yaml
workflow:
  transcript:
    enabled: true
    max_assistant_chars: 4000
    max_tool_output_chars: 2000
    max_stdout_chars: 4000
    max_stderr_chars: 4000
```

#### workflow.credentials (#970)

Define default credentials available to all workflow runs in this config layer. Workflow definitions can override with their own `credentials` section.

```yaml
workflow:
  credentials:
    - name: github_token
      env_var: GITHUB_TOKEN
    - name: deploy_key
      command: "op read op://vault/keys/deploy"
```

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes | Credential identifier |
| `env_var` | string | no | Environment variable to look up (must be non-empty) |
| `command` | string | no | Shell command to execute (must exit 0 and produce output) |
| `allowed_runners` | list | no | Limit to specific runner types (e.g., `["shell"]`); `null` = all |

Rules:

- Exactly one of `env_var` or `command` must be provided per credential
- Resolved values are never logged, stored, or included in error messages
- Resolution happens at step runtime when a step's `credentials` field references a name
- Resolution failures block the step with a redacted error

### references

Paths to shared instruction, rule, and skill files. Typically used in team or project configs to share development standards. See [Project Configuration](project-config.md).

| Field | Type | Default | Description |
|---|---|---|---|
| `instructions` | list | `[]` | Paths to instruction files (markdown) loaded as context |
| `rules` | list | `[]` | Paths to rule files (markdown) loaded as behavioral constraints |
| `skills` | list | `[]` | Paths to skill files (markdown) loaded as reusable prompts |

Paths are resolved relative to the config file that declares them. Non-string entries are silently filtered out.

### required

A list of config keys that must be present in the user's personal config. Typically used in team or project configs. See [Project Configuration](project-config.md#required-keys).

Each entry is a dict with:

| Field | Type | Required | Description |
|---|---|---|---|
| `path` | string | yes | Dot-separated config path (e.g., `ai.api_key`) |
| `description` | string | no | Human-readable description shown during prompting |

In interactive mode, missing required keys trigger a prompt. Sensitive fields (containing `key`, `secret`, `password`, `token`, `passphrase`) use masked input. Values are saved to the personal config with 0600 permissions.

In non-interactive mode, missing keys produce an error message listing each missing path and its equivalent `AI_CHAT_*` environment variable.

### pack_sources

Configure git repositories containing packs for automatic cloning and refresh.

```yaml
pack_sources:
  - url: https://github.com/acme/anteroom-packs.git
    branch: main
    refresh_interval: 30  # minutes; 0 = manual only
    auto_attach: true     # automatically attach packs from this source
    priority: 50          # 1-100, lower wins for conflict resolution
```

| Field | Type | Default | Description |
|---|---|---|---|
| `url` | string | (required) | Git remote URL. Accepts `https://`, `ssh://`, `git://`, and SSH shorthand (`git@host:path`). Rejects `http://`, `ext::`, and `file://` |
| `branch` | string | `"main"` | Git branch to clone and track |
| `refresh_interval` | integer | `30` | Minutes between auto-refresh. `0` = manual only. Minimum: 5 (values below 5 are clamped) |
| `auto_attach` | boolean | `true` | When `true`, new packs from this source are automatically attached at the global scope on install. When `false`, packs must be manually attached via `aroom pack attach` |
| `priority` | integer | `50` | Conflict resolution priority (1-100). Lower number wins when multiple sources provide conflicting packs. Must be in range 1-100 |

The background worker clones sources on first encounter, then pulls periodically. After 10 consecutive background refresh loop failures, the worker stops all source refreshes until restart. See [Pack Sources](../packs/pack-sources.md) for the full lifecycle.

## API Key Command

The `api_key_command` field runs an external command to obtain API keys with automatic transparent refresh:

- Command is executed via `subprocess.run()` with `shlex.split()` --- no `shell=True`, preventing shell injection
- 30-second execution timeout prevents hanging commands
- Token is cached in memory only, never written to disk or logged
- On HTTP 401, the command is re-run automatically and the request is retried

```yaml
ai:
  api_key_command: "aws secretsmanager get-secret-value --secret-id anteroom-key --query SecretString --output text"
```
