Skip to content
Configuration Reference

Configuration Reference

Loop is configured via HJSON files (JSON with comments and trailing commas). This document covers all configuration fields, their defaults, and the merge rules for project-level overrides.

Config files can be edited directly on disk, or through the HTTP API (GET/PUT /api/config for global, GET/PUT /api/config/project for per-project). The desktop app Settings panel uses the API to provide a schema-driven form editor and a raw JSON editor. See API Reference: Configuration for endpoint details.

See also: Docker Container Lifecycle , Task Scheduling .


Global Config

Location: ~/.loop/config.json

This is the primary configuration file, loaded at startup. All paths below are relative to the ~/.loop/ directory (referred to as loopDir).

All Fields

Platforms & Credentials

FieldTypeDefaultDescription
platformsstring[](required)One or more of "discord", "slack", "local". Multiple platforms can run simultaneously.
discord_tokenstring""Discord bot token. Required when "discord" is in platforms.
discord_app_idstring""Discord application ID. Required when "discord" is in platforms.
discord_guild_idstring""Discord guild (server) ID. Enables auto-creation of Discord channels via loop mcp --dir.
slack_bot_tokenstring""Slack bot token (xoxb-...). Required when "slack" is in platforms.
slack_app_tokenstring""Slack app-level token (xapp-...). Required when "slack" is in platforms.

The "local" platform requires no external credentials – it runs with the Electron desktop app as the UI.

Authentication

Exactly one of these should be set. OAuth takes precedence if both are provided.

FieldTypeDefaultDescription
claude_code_oauth_tokenstring""OAuth token from claude setup-token. Uses your Claude subscription.
anthropic_api_keystring""Anthropic API key. Uses pay-per-token API pricing.

Claude & Agent

FieldTypeDefaultDescription
claude_bin_pathstring"claude"Path to the Claude CLI binary inside containers.
claude_modelstring"claude-sonnet-4-6"Claude model to use. Options: "claude-opus-4-7", "claude-opus-4-6[1m]", "claude-opus-4-6", "claude-sonnet-4-6".
streaming_enabledbooltrueStream intermediate Claude turns to chat as they happen.
keep_mcp_configsboolfalseWhen true, preserves MCP config JSON files after container runs. Useful for debugging MCP server configuration.

Storage & Logging

FieldTypeDefaultDescription
db_pathstring"~/.loop/loop.db"SQLite database path.
log_filestring"~/.loop/loop.log"Log file path.
log_levelstring"info"Log level (debug, info, warn, error).
log_formatstring"text"Log format (text or json).

Container Settings

FieldTypeDefaultDescription
container_imagestring"loop-agent:latest"Docker image for agent containers.
container_timeout_secint3600Maximum execution time per container run (seconds).
container_memory_mbint1024Memory limit per container (MB).
container_cpusfloat1.0CPU limit per container (fractional cores).
container_keep_alive_secint300Seconds to keep a finished container before removal (for docker logs debugging).

Browser Automation

FieldTypeDefaultDescription
Browser settings are grouped under "browser":
FieldTypeDefaultDescription
browser.enabledbooltrueEnable Chrome browser automation. When disabled, no Chrome container is started and the loop-browser MCP server is not registered.
browser.chrome_imagestring"loop-chrome:latest"Docker image for Chrome sidecar containers.
browser.host_cdp_portint9222CDP port for Host mode. Requires chrome://inspect/#remote-debugging enabled in Chrome.

Networking & Scheduling

FieldTypeDefaultDescription
api_addrstring":8222"HTTP API listen address.
poll_interval_secint30How often the scheduler checks for due tasks (seconds).

GitHub

FieldTypeDefaultDescription
github.gh_userstring""Named gh CLI account to use for PR lookups (see gh auth status). Empty falls back to whichever account gh currently has active. Used by GET /api/channels/{id}/pr and the Git panel’s PR chip.

Mounts

"mounts": [
  "~/.claude:~/.claude",
  "~/.gitconfig:~/.gitconfig:ro",
  "~/.ssh:~/.ssh:ro",
  "/var/run/docker.sock:/var/run/docker.sock",
  "loop-npmcache:~/.npm",  // Docker named volume
  "loop-gocache:/go"        // Docker named volume
]

Format: host_path:container_path[:mode]

  • ~ is expanded to the host user’s home directory on both sides.
  • Named volumes (no /, ~, or . prefix) are passed to Docker directly.
  • Non-existent host paths are silently skipped.
  • See Containers: Mount Processing for full details.

Copy Files

"copy_files": ["~/.claude.json"]
FieldTypeDefaultDescription
copy_filesstring[]["~/.claude.json"]Files copied into each container (not mounted). Each container gets its own independent copy.

See Containers: File Copying .

Custom Environment Variables

"envs": {
  "BASH_ENV": "~/.bashrc",
  "ENABLE_LSP_TOOL": false
}
FieldTypeDefaultDescription
envsmap[string]anynullExtra environment variables passed to all containers. Values can be any JSON type; non-strings are stringified. ~ in values is expanded.

MCP Servers

"mcp": {
  "servers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token"
      }
    }
  }
}

Each server entry has:

FieldTypeDescription
commandstringExecutable to run the MCP server.
argsstring[]Command-line arguments.
envmap[string]stringEnvironment variables for the server process.

A built-in "loop" server is always added unless the user defines one with the same name. See Containers: MCP Config Generation .

Task Templates

"task_templates": [
  {
    "name": "daily-summary",
    "description": "Generate a daily summary",
    "schedule": "0 17 * * *",
    "type": "cron",
    "prompt": "Summarize today's activity",
    "auto_delete_sec": 60
  },
  {
    "name": "heartbeat",
    "description": "Periodic health check",
    "schedule": "30m",
    "type": "interval",
    "prompt_path": "heartbeat.md"
  }
]
FieldTypeDescription
namestringUnique template identifier.
descriptionstringHuman-readable description shown in template listings.
schedulestringCron expression, Go duration, or RFC3339 timestamp (depends on type).
typestringOne of "cron", "interval", "once".
promptstringInline prompt text. Mutually exclusive with prompt_path.
prompt_pathstringPath to a prompt file, resolved as ~/.loop/templates/{prompt_path}. Mutually exclusive with prompt.
origin_branchstringBase branch for worktree tasks. If omitted, auto-detected from the parent repo on first run.
update_before_runboolWhen true, prepends git fetch/rebase instructions to the prompt before each run. Default: false.
auto_delete_secintSeconds after execution to auto-delete the task’s thread. 0 disables.

See Task Scheduling for full details.

Prompt Shortcuts

"prompt_shortcuts": [
  {
    "name": "coverage",
    "description": "Run coverage check",
    "prompt": "Run make coverage-check and report results"
  },
  {
    "name": "review",
    "description": "Review uncommitted and branch changes",
    "prompt_path": "review-code.md"
  }
]
FieldTypeDescription
namestringUnique shortcut identifier. Shown in the # picker in chat.
descriptionstringHuman-readable description shown below the name.
promptstringInline prompt text. Mutually exclusive with prompt_path.
prompt_pathstringPath to a prompt file, resolved as ~/.loop/shortcuts/{prompt_path} (global) or .loop/shortcuts/{prompt_path} (project). Mutually exclusive with prompt.

Shortcuts appear in the chat input when the user types #. Selecting a shortcut sends its resolved prompt as a message. The API endpoint GET /api/shortcuts returns all shortcuts with resolved prompts; pass ?channel_id=<id> to merge project-level shortcuts. Agents can manage shortcuts via the prompt_shortcut MCP tool or the POST /api/shortcuts endpoint — add, update, or delete shortcuts in either global or project scope.

Review

Enables and configures the Review panel (see review.md ).

"review": {
  "enabled": true,
  "prompt_path": "review.md"
}
FieldTypeDescription
enabledboolGates the Review panel. false (the default) hides the panel in the FE picker and makes the backend reject /review/* requests with 403. Layered per-global/per-project/per-worktree like github.gh_user.
promptstringInline prompt text. Mutually exclusive with prompt_path.
prompt_pathstringPath to a prompt file, resolved as ~/.loop/review/{prompt_path}. Mutually exclusive with prompt.

Both prompt fields empty (the default) uses the daemon’s built-in default prompt, which instructs the agent to emit <review-comment> blocks for actionable issues only.

Workflows

"workflows": [
  {
    "name": "fix-issue",
    "description": "Analyze an issue, plan, implement, and create a PR",
    "inputs": {
      "issue_url": { "description": "Issue URL", "required": true }
    },
    "nodes": [
      { "id": "analyze", "type": "bash", "script": "gh issue view {{.Inputs.issue_url}} --json title,body,labels" },
      { "id": "plan", "type": "prompt", "depends_on": ["analyze"], "prompt": "Create a plan:\n\n{{.NodeOutputs.analyze}}" },
      { "id": "implement", "type": "prompt", "depends_on": ["plan"], "prompt": "Implement:\n\n{{.NodeOutputs.plan}}" },
      { "id": "pr", "type": "prompt", "depends_on": ["implement"], "prompt": "Commit and create a PR" }
    ]
  }
]
FieldTypeDescription
namestringUnique workflow identifier.
descriptionstringHuman-readable description.
timeoutstringGo duration (e.g. "30m") that caps total DAG execution time. Run fails with "workflow timeout exceeded" on expiry.
inputsmap[string]WorkflowInputNamed inputs with description, required, and default fields.
nodesNodeDef[]Ordered list of DAG nodes.

Node fields:

FieldTypeDescription
idstringUnique node identifier within the workflow.
typestring"prompt", "bash", "loop", or "approval".
depends_onstring[]IDs of nodes that must complete before this one starts.
promptstringPrompt text for prompt/loop nodes. Supports Go text/template. Mutually exclusive with prompt_path.
prompt_pathstringPath to a prompt file, resolved as {loopDir}/workflows/{prompt_path}. Mutually exclusive with prompt.
system_promptstringOptional system prompt for prompt nodes. Supports templates.
scriptstringShell command(s) for bash nodes, passed to /bin/sh -c. Any sh-compatible content — one-liners, multi-line scripts, pipelines, heredocs. To run a script file on disk, just invoke it (e.g. bash workflows/build.sh); the bash container shares the same mounts as agent containers. Supports templates.
max_iterationsintMax iterations for loop nodes (default: 10).
conditionstringTemplate for loop nodes; stops when it renders "true".
messagestringApproval message for approval nodes. Supports templates.
timeoutstringGo duration (e.g. "5m", "1h"). For approval nodes: deadline for human response. For prompt/bash/loop nodes: enforced execution deadline via context cancellation.
retryRetryConfigOptional retry with max_retries, backoff_base, backoff_max.
whenstringTemplate that must evaluate to "true" for the node to run. Skipped otherwise.
trigger_rulestring"all_success" (default), "all_done", or "one_success". Controls how dependency failures affect this node.

See Workflows for architecture details and the DAG execution model.

Workflow Concurrency

"workflow_concurrency": {
  "max_concurrent_runs": 5,
  "max_concurrent_nodes": 10
}
FieldTypeDescription
max_concurrent_runsintMaximum workflow runs executing in parallel. 0 = unlimited. Default: 0.
max_concurrent_nodesintMaximum node goroutines across all active runs. 0 = unlimited. Default: 0.

Available at both global and project level. Project values override global values when > 0.

Memory

"memory": {
  "enabled": true,
  "paths": ["./memory", "!./memory/plans"],
  "max_chunk_chars": 5000,
  "reindex_interval_sec": 300,
  "embeddings": {
    "provider": "ollama",
    "model": "nomic-embed-text",
    "ollama_url": "http://localhost:11434"
  }
}
FieldTypeDefaultDescription
memory.enabledboolfalseMust be explicitly true to enable semantic memory search.
memory.pathsstring[]["./memory"]Directories to index. Paths can be absolute or relative to the project work dir. Prefix with ! to exclude.
memory.max_chunk_charsint5000Maximum characters per embedding chunk.
memory.reindex_interval_secint300Periodic re-index interval in seconds (5 minutes default).
memory.embeddings.providerstring""Embedding provider. Currently only "ollama" is supported.
memory.embeddings.modelstring""Embedding model name (e.g. "nomic-embed-text").
memory.embeddings.ollama_urlstring"http://localhost:11434"Ollama API endpoint.

Quality

"quality": {
  "max_files": 25000,
  "exclude_paths": ["docs/**"],
  "complexity": {
    "cyclomatic_t": 10,
    "cognitive_t":  15,
    "nesting_t":    4,
    "params_t":     5,
    "loc_t":        60
  },
  "clones": {
    "min_loc":      5,
    "max_distance": 3
  },
  "rules": {
    "signal_floor":            { "enabled": true, "threshold": 5000 },
    "parse_fail":              { "enabled": true, "threshold": 0.01 },
    "no_import_cycles":        { "enabled": true },
    "complexity_ceiling":      { "enabled": true, "threshold": 10 },
    "complexity_score_floor":  { "enabled": true, "threshold": 0.5 },
    "duplication_ceiling":     { "enabled": true, "threshold": 0.10 }
  }
}

Scans are manual: panel “Scan now” button, loop quality scan, or the quality_scan MCP tool.

FieldTypeDefaultDescription
quality.max_filesint25000Hard cap on scannable files after exclusions. Over the cap returns a structured RepoTooLarge error; no partial scan is produced.
quality.exclude_pathsstring[][]Doublestar globs appended to the built-in defaults (.git/, node_modules/, dist/, build/, target/, vendor/, *.min.js, *.generated.go) and the repo’s .gitignore.
quality.complexity.cyclomatic_tint10Soft threshold for McCabe cyclomatic complexity. Above T the dimension scores T/raw (0.50 at 2T, 0.10 at 10T, asymptotic to 0). 0 disables the dimension.
quality.complexity.cognitive_tint15Soft threshold for Sonar cognitive load. Same T/raw curve.
quality.complexity.nesting_tint4Soft threshold for max nesting depth.
quality.complexity.params_tint5Soft threshold for parameter count.
quality.complexity.loc_tint60Soft threshold for function LOC.
quality.clones.min_locint5Minimum function LOC to be considered for clone clustering. Below this, functions are skipped to avoid trivial getter/setter clusters.
quality.clones.max_distanceint3Max SimHash Hamming distance between two functions in the same cluster. 0 requires exact-shape duplicates; 64 clusters anything.
quality.rules.<name>.enabledbooltruePer-rule on/off. Built-in rules: no_import_cycles, signal_floor, parse_fail, complexity_ceiling, complexity_score_floor, duplication_ceiling.
quality.rules.signal_floor.thresholdfloat5000Lower bound on quality_signal (0–10000). Below this, the rule fails.
quality.rules.parse_fail.thresholdfloat0.01Maximum fraction of files allowed to fail to parse (0.01 = 1%).
quality.rules.complexity_ceiling.thresholdint10Maximum number of functions allowed to breach any complexity soft threshold before this rule fails.
quality.rules.complexity_score_floor.thresholdfloat0.5Lower bound on the complexity metric’s headline score (0–1).
quality.rules.duplication_ceiling.thresholdfloat0.10Maximum tolerated duplicated_loc / total_loc ratio across clone clusters.

Config changes drop the engine’s parser/graph cache for the affected channel but do not auto-trigger a rescan — the panel keeps rendering the previous snapshot until the next manual or live scan rebuilds with the new config. See Quality for the full surface.

Permissions (RBAC)

"permissions": {
  "owners": { "users": ["U12345678"], "roles": ["1234567890123456789"] },
  "members": { "users": [], "roles": [] }
}
FieldTypeDescription
permissions.owners.usersstring[]User IDs with owner access (full control including permission management).
permissions.owners.rolesstring[]Role IDs with owner access.
permissions.members.usersstring[]User IDs with member access (can trigger bot and manage tasks).
permissions.members.rolesstring[]Role IDs with member access.

If all config and DB permissions are empty, everyone is treated as an owner (bootstrap mode).

Security Gate

The gate runs a seccomp RET_USER_NOTIF filter inside each agent container and forwards approve-rule traps to Discord / Slack / the desktop. See HTTP API: Gate Approvals for the resolve endpoint and Events: gate.approval_requested for the WebSocket payload.

"gates": {
  "rate_limits": { "pending": 30, "per_minute": 60, "total": 500 },
  "audit":       { "retention_days": 30, "verbose": false },

  "agentgate": {
    "enabled": true,
    "default_decision": "allow",
    "path_rules":    [ /* unix-socket connect rules */ ],
    "command_rules": [ /* execve/execveat rules */ ],
    "file_rules":    [ /* openat/renameat2/unlinkat rules */ ]
  },

  "docker_proxy": {
    "enabled": true,
    "default_decision": "approve",
    "http_rules": [ /* per-method/path rules */ ],
    "body_rules": [ /* JSON-body inspection for container-create etc. */ ]
  }
}

gates is the single umbrella for both enforcement layers — the kernel-level agentgate (seccomp) and the application-level docker_proxy (Docker HTTP). Shared resources (rate_limits, audit) live at the umbrella level because both layers feed into the same agentgate.Manager for approvals.

FieldTypeDefaultDescription
gates.rate_limits.pendingint30Max simultaneously-pending prompts per container; new requests over this auto-deny. Shared across both gate layers.
gates.rate_limits.per_minuteint60Max distinct-target prompts per minute per container.
gates.rate_limits.totalint500Per-container lifetime cap; exceeded = deny-by-default for the rest of the run.
gates.audit.retention_daysint30Days of gate decision logs to retain on disk. FileAuditor prunes older agentgate-YYYY-MM-DD.jsonl files on each rotation; 0 or negative disables pruning. Surfaced into the container as LOOP_GATE_AUDIT_RETENTION_DAYS.
gates.audit.verboseboolfalseWhen false (default), FileAuditor drops silent allows (policy said allow, nobody was prompted) and cache-hit allows — only denies (silent or prompted) and user-clicked decisions are logged, keeping the trail focused on events operators actually want to review. When true, every decision is logged, including silent allows and cache hits — use for debugging rule authoring or exporting a full trace to a SIEM. Surfaced into the container as LOOP_GATE_AUDIT_VERBOSE=1.
gates.agentgate.enabledbooltrueMaster switch for the seccomp gate. When false, no filter is installed and no gate policy file is bind-mounted. Transitively disables gates.docker_proxy unless explicitly overridden.
gates.agentgate.default_decisionstring"allow""allow" or "approve". Fallback when no rule matches.
gates.agentgate.path_rules[]PathRule1 rule (docker.sock → approve)Rules for unix-socket connects, matched by absolute socket path. User rules are appended to the shipped baseline.
gates.agentgate.command_rules[]CommandRule2 rules (rm -rf / deny, git push etc. approve)Rules for execve/execveat, matched by basename glob and argv regex. User rules are appended to the baseline.
gates.agentgate.file_rules[]FileRule8 rulesRules for file ops (openat, renameat2, unlinkat, linkat, symlinkat, fchmodat, fchownat, mkdirat), matched by doublestar path glob × operation set. User rules are appended.

Rule field shapes:

// PathRule
{ "pattern": "/var/run/docker.sock", "decision": "approve", "message": "agent wants to use docker" }

// CommandRule — empty commands or args_patterns means "match any"
{ "commands": ["git"], "args_patterns": ["^(push|remote add|reset --hard) .*"], "decision": "approve", "message": "git write op" }

// FileRule — empty paths or operations means "match any"
{ "paths": ["**/.ssh/**"], "operations": ["read","write","create","delete","chmod"], "decision": "deny", "message": "credentials blocked" }

decision is one of "allow", "deny", or "approve". approve blocks the syscall and broadcasts a gate.approval_requested event; the UI’s three-button card resolves it via POST /api/gate/approvals/{id} .

FieldTypeDefaultDescription
gates.docker_proxy.enabledboolmirrors gates.agentgate.enabledReplaces the container’s direct /var/run/docker.sock bind with an in-container HTTP proxy (loop dockerproxy) that listens on /var/run/docker.sock (tmpfs) and reverse-proxies to the real daemon socket at /var/run/docker.sock.host while enforcing http_rules + body_rules.
gates.docker_proxy.default_decisionstring"approve"Fallback when no http_rules entry matches. "allow", "approve", or "deny".
gates.docker_proxy.http_rules[]HTTPServiceRule15 rulesMethod × path-regex rules. User rules are appended to the baseline (GET reads allowed; POST create/start/exec/update approved; swarm/secrets/plugins denied).
gates.docker_proxy.body_rules[]BodyRule2 rulesJSON-body inspection rules on POST /containers/create and /update. Baseline denies root-level bind-mounts (/, /etc, /root, /proc, /sys, /dev, /var/run/docker.sock), Privileged, host-namespace flags, dangerous CapAdd, --security-opt=*unconfined, Devices/VolumesFrom, and MaskedPaths: []. Body-rule decisions follow the same semantics as http_rules: deny returns 403 with no prompt, approve blocks and prompts the user, allow passes through silently.

Desktop (Electron App)

"desktop": {
  "theme": "dark",
  "islands": true,
  "preview_tabs": true,
  "auto_save_on_blur": false,
  "stop_daemon_on_quit": false,
  "font_sizes": {
    "sidebar": 12,
    "chat": 13,
    "terminal": 13,
    "editor": 13,
    "panels": 12
  }
}
FieldTypeDefaultDescription
desktop.themestring"dark"Color theme. One of "dark", "light", "claude".
desktop.islandsbooltrueIslands layout — panels float as rounded cards over a deep canvas with gaps between them.
desktop.preview_tabsbooltrueSingle-click opens files in a transient preview tab. Double-click promotes to permanent.
desktop.auto_save_on_blurboolfalseSave open editor tabs when the window loses focus.
desktop.stop_daemon_on_quitboolfalseUninstalls the daemon service on quit. Re-installed on next app launch.
desktop.font_sizesobjectSee abovePer-area font size overrides (in px). Keys: sidebar, chat, terminal, editor, panels.

These settings are global-only (not available in project configs). Changes are applied live when saving the config — no restart required.


Project Config

Location: {workDir}/.loop/config.json

Project-level configs allow per-project overrides. They are loaded and merged with the global config whenever a container is created for a channel that has a custom work directory.

Available Fields

Not all global fields are available in project configs. The following fields can be set:

FieldMerge Behavior
mountsReplaces global mounts entirely. Relative host paths are resolved relative to workDir.
copy_filesReplaces global copy_files entirely when set.
mcp.serversMerged with global servers. Project servers override global servers with the same name.
envsMerged with global envs. Project values override global values with the same key.
claude_modelOverrides global value when set.
claude_bin_pathOverrides global value when set.
claude_code_oauth_tokenOverrides global auth entirely. Clears anthropic_api_key.
anthropic_api_keyOverrides global auth entirely. Clears claude_code_oauth_token.
container_imageOverrides global value when set.
container_memory_mbOverrides global value when set.
container_cpusOverrides global value when set.
keep_mcp_configsOverrides global value when set.
memory.pathsAppended to global memory paths.
memory.max_chunk_charsOverrides global value when set (> 0).
memory.embeddingsOverrides global embeddings config entirely when set.
permissionsReplaces global permissions entirely when set.
task_templatesMerged by name. Project templates override global templates with the same name; new names are appended.
prompt_shortcutsMerged by name. Project shortcuts override global shortcuts with the same name; new names are appended.
workflowsMerged by name. Project workflows override global workflows with the same name; new names are appended.
workflow_concurrency.max_concurrent_runsOverrides global value when > 0.
workflow_concurrency.max_concurrent_nodesOverrides global value when > 0.
browser.enabledOverrides global value when set.
browser.chrome_imageOverrides global value when set.
browser.host_cdp_portOverrides global value when set.
github.gh_userOverrides global value when set.
gates.agentgate.enabledNarrows only: project may set false to disable the gate for this project; it cannot re-enable the gate when global gates.agentgate.enabled is false. Transitively disables gates.docker_proxy.enabled when the project turns the gate off.
gates.agentgate.path_rules / command_rules / file_rulesPrepended to the merged global rules (first-match-wins applies project rules first). Rules with decision: "allow" are rejected at load time — project configs may only tighten the policy, not loosen it.
gates.agentgate.default_decisionIgnored — global wins unconditionally.
gates.docker_proxy.enabledSame narrow rule as gates.agentgate.enabled: project can disable (not re-enable).
gates.docker_proxy.http_rules / body_rulesPrepended to the merged global rules. Rules with decision: "allow" are rejected at load time.
gates.docker_proxy.default_decisionIgnored — global wins.
gates.rate_limits / gates.auditIgnored — global wins unconditionally.

Project Config Merge Rules

The merge follows these principles:

  • Replace: The project value completely replaces the global value (mounts, copy_files, permissions).
  • Merge: Both global and project values are combined, with project taking precedence on conflicts (MCP servers, envs, task templates, workflows).
  • Append: Project values are added to the global list (memory paths).
  • Override: A single scalar value replaces the global one (claude_model, container_image, etc.).
  • Narrow merge: Security-sensitive fields under gates (agentgate, docker_proxy) have a locked-down merge: project rules prepend, allow rules are rejected at load, and default_decision / rate_limits / audit are ignored so a compromised project file cannot loosen global policy.
  • Absent = inherit: If a field is not set in the project config, the global value is used unchanged.

Complete Example Config

{
  // Required: platform(s) to run
  "platforms": ["local"],

  // Discord credentials (required when platform is "discord")
  "discord_token": "your-discord-bot-token-here",
  "discord_app_id": "your-discord-app-id-here",
  "discord_guild_id": "your-discord-guild-id-here",

  // Slack credentials (required when platform is "slack")
  //"slack_bot_token": "xoxb-your-slack-bot-token",
  //"slack_app_token": "xapp-your-slack-app-token",

  // Authentication (use one; OAuth takes precedence)
  //"claude_code_oauth_token": "sk-ant-your-oauth-token-here",
  //"anthropic_api_key": "sk-ant-your-api-key-here",

  // Storage & logging
  //"db_path": "~/.loop/loop.db",
  //"log_file": "~/.loop/loop.log",
  //"log_level": "info",
  //"log_format": "text",

  // Container settings
  //"container_image": "loop-agent:latest",
  //"container_timeout_sec": 3600,
  //"container_memory_mb": 1024,
  //"container_cpus": 1.0,
  //"container_keep_alive_sec": 300,

  // Scheduling & networking
  //"poll_interval_sec": 30,
  //"api_addr": ":8222",

  // Agent settings
  //"claude_model": "",
  //"claude_bin_path": "claude",
  //"streaming_enabled": true,
  //"keep_mcp_configs": false, // preserve MCP config files after container runs for debugging

  // Browser automation
  //"browser": {
  //  "enabled": true,
  //  "chrome_image": "loop-chrome:latest",
  //  "host_cdp_port": 9222
  //},

  // GitHub integration (used by the Git panel's PR chip)
  //"github": {
  //  "gh_user": "" // named gh CLI account; empty = gh's currently-active account
  //},

  // RBAC permissions
  //"permissions": {
  //  "owners":  { "users": ["U12345678"], "roles": ["1234567890123456789"] },
  //  "members": { "users": [], "roles": [] }
  //},

  // Semantic memory search
  "memory": {
    "enabled": true,
    "paths": ["./memory", "!./memory/plans"],
    //"max_chunk_chars": 5000,
    //"reindex_interval_sec": 300,
    "embeddings": {
      "provider": "ollama",
      "model": "nomic-embed-text"
      //"ollama_url": "http://localhost:11434"
    }
  },

  // MCP servers
  "mcp": {
    //"servers": {
    //  "github": {
    //    "command": "npx",
    //    "args": ["-y", "@modelcontextprotocol/server-github"],
    //    "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token" }
    //  }
    //}
  },

  // Custom environment variables for containers
  "envs": {
    "BASH_ENV": "~/.bashrc",
    "ENABLE_LSP_TOOL": false
  },

  // Files copied into containers (not mounted)
  "copy_files": ["~/.claude.json"],

  // Container mounts
  "mounts": [
    "~/.claude:~/.claude",
    "~/.gitconfig:~/.gitconfig:ro",
    "~/.ssh:~/.ssh:ro",
    "~/.aws:~/.aws",
    "/var/run/docker.sock:/var/run/docker.sock",
    "~/.loop/.bashrc:~/.bashrc:ro",
    "loop-npmcache:~/.npm",
    "loop-uvcache:~/.local/share/uv",
    "loop-cache:~/.cache",
    "loop-gocache:/go",
    "loop-ollama:~/.ollama"
  ],

  // Task templates
  "task_templates": [
    {
      "name": "daily-summary",
      "description": "Generate a daily summary of completed tickets",
      "schedule": "0 17 * * *",
      "type": "cron",
      "prompt": "Generate a summary of all tickets closed today."
    },
    {
      "name": "heartbeat",
      "description": "Periodic health check",
      "schedule": "30m",
      "type": "interval",
      "prompt_path": "heartbeat.md",
      "auto_delete_sec": 60
    }
  ],

  // Prompt shortcuts (triggered via # in chat)
  "prompt_shortcuts": [
    {
      "name": "coverage",
      "description": "Run coverage check",
      "prompt": "Run make coverage-check and report the results"
    },
    {
      "name": "review",
      "description": "Review uncommitted and branch changes",
      "prompt_path": "review-code.md"
    }
  ],

  // Workflows — declarative DAG pipelines
  "workflows": [],

  // Workflow concurrency limits (0 = unlimited)
  "workflow_concurrency": {
    "max_concurrent_runs": 0,
    "max_concurrent_nodes": 0
  },

  // Security gates umbrella. Both layers enabled by default with a baseline
  // deny-list; uncomment to extend. User rules append to the shipped baseline.
  //"gates": {
  //  //"rate_limits": { "pending": 30, "per_minute": 60, "total": 500 },
  //  //"audit":       { "retention_days": 30, "verbose": false },
  //
  //  // Kernel-level seccomp gate.
  //  //"agentgate": {
  //  //  "enabled": true,
  //  //  "default_decision": "allow",
  //  //  //"path_rules":    [ { "pattern": "/custom/socket", "decision": "deny" } ],
  //  //  //"command_rules": [ { "commands": ["curl"], "args_patterns": ["evil\\.com"], "decision": "deny" } ],
  //  //  //"file_rules":    [ { "paths": ["/project-secret/**"], "operations": ["read","write"], "decision": "deny" } ]
  //  //},
  //
  //  // Docker HTTP proxy (enabled automatically when agentgate.enabled). Replaces the
  //  // direct docker.sock bind with an in-container proxy that enforces per-method
  //  // and body-level rules.
  //  //"docker_proxy": {
  //  //  "enabled": true,
  //  //  "default_decision": "approve"
  //  //  //"http_rules": [ { "methods": ["POST"], "paths": ["^/my/route$"], "decision": "deny" } ]
  //  //}
  //}
}

Project Example Config

{
  // Claude model override for this project
  //"claude_model": "claude-opus-4-6",

  // Claude binary path override
  //"claude_bin_path": "/usr/local/bin/claude",

  // Auth override (overrides global auth entirely)
  //"claude_code_oauth_token": "sk-ant-project-oauth-token",
  //"anthropic_api_key": "sk-ant-project-api-key",

  // Container overrides
  //"container_image": "loop-agent:latest",
  //"container_memory_mb": 2048,
  //"container_cpus": 2.0,

  // Preserve MCP config files after container runs for debugging
  //"keep_mcp_configs": false,

  // Browser automation override
  //"browser": {
  //  "enabled": false,
  //  "chrome_image": "loop-chrome:latest"
  //},

  // GitHub override (per-project gh CLI account)
  //"github": {
  //  "gh_user": ""
  //},

  // Memory config (paths appended to global; embeddings override global)
  //"memory": {
  //  "paths": ["./docs/architecture.md", "!./docs/wip"],
  //  "max_chunk_chars": 5000,
  //  "embeddings": { "provider": "ollama", "model": "nomic-embed-text" }
  //},

  // Files copied into containers (replaces global when set)
  //"copy_files": ["~/.claude.json"],

  // Project-specific MCP servers (merged with global; project overrides by name)
  //"mcp": {
  //  "servers": {
  //    "my-tool": {
  //      "command": "/path/to/binary",
  //      "args": ["--flag"],
  //      "env": { "API_KEY": "secret" }
  //    }
  //  }
  //},

  // Extra env vars (merged with global; project overrides by key)
  //"envs": {},

  // Project mounts (replaces global mounts; relative paths resolved to project dir)
  //"mounts": [
  //  "~/.claude:~/.claude",
  //  "~/.gitconfig:~/.gitconfig:ro",
  //  "~/.ssh:~/.ssh:ro"
  //],

  // Permissions override (replaces global permissions when set)
  //"permissions": {
  //  "owners":  { "users": [], "roles": [] },
  //  "members": { "users": [], "roles": [] }
  //},

  // Task templates (merged by name; project overrides global templates with same name)
  //"task_templates": [
  //  {
  //    "name": "daily-summary",
  //    "description": "Project-specific daily summary",
  //    "schedule": "0 18 * * *",
  //    "type": "cron",
  //    "prompt": "Summarize today's project activity"
  //  }
  //],

  // Prompt shortcuts (merged by name; project overrides global shortcuts with same name)
  //"prompt_shortcuts": [
  //  {
  //    "name": "lint",
  //    "description": "Run linter",
  //    "prompt": "Run make lint and fix any issues"
  //  }
  //],

  // Workflows (merged by name; project overrides global workflows with same name)
  //"workflows": [
  //  {
  //    "name": "code-review",
  //    "description": "Review branch changes",
  //    "nodes": [
  //      { "id": "diff", "type": "bash", "script": "git diff main...HEAD" },
  //      { "id": "review", "type": "prompt", "depends_on": ["diff"], "prompt": "Review:\n\n{{.NodeOutputs.diff}}" }
  //    ]
  //  }
  //],

  // Workflow concurrency limits (overrides global values)
  //"workflow_concurrency": {
  //  "max_concurrent_runs": 5,
  //  "max_concurrent_nodes": 10
  //},

  // Security gates override for this project only. Can disable (not re-enable);
  // rules prepend to global and must use decision: "deny" or "approve"
  // ("allow" is rejected at load time).
  //"gates": {
  //  //"agentgate": {
  //  //  "enabled": false,
  //  //  //"command_rules": [ { "commands": ["npm"], "args_patterns": ["^publish"], "decision": "deny" } ],
  //  //  //"file_rules":    [ { "paths": ["./secret-vault/**"], "operations": ["read"], "decision": "deny" } ]
  //  //},
  //
  //  //"docker_proxy": {
  //  //  //"http_rules": [ /* project-specific Docker API restrictions */ ]
  //  //}
  //}
}