Skip to content

Configuration#

Installation#

Installation has moved to its own page. See Install ECA for a guided walkthrough covering editor plugins (Emacs, VS Code, Neovim, IntelliJ), the Desktop app, and manual install methods (script, Homebrew, mise, GitHub Releases).

Ways to configure#

There are multiples ways to configure ECA:

Convenient for users and multiple projects

~/.config/eca/config.json
{
  "defaultAgent": "plan"
}

Convenient for users

.eca/config.json
{
  "defaultAgent": "plan"
}

Convenient for editors but uncommon

Client editors can pass custom settings when sending the initialize request via the initializationOptions object:

"initializationOptions": {
  "defaultAgent": "plan"
}

Via env var during server process spawn:

ECA_CONFIG='{"myConfig": "my_value"}' eca server

Merge extra files last, useful for machine-specific or uncommitted overrides (e.g. gitignored *.local.json files):

~/.config/eca/config.json
{
  "extraConfigs": ["~/.config/eca/config.local.json", ".eca/config.local.json"]
}

Relative paths resolve against the workspace root; ~ and absolute paths are supported. Missing files are logged and skipped.

Merge order#

Config sources are deep merged from lowest to highest priority (later sources win on conflicts):

  1. Built-in defaults
  2. initializationOptions (from the editor's initialize request)
  3. ECA_CONFIG env var
  4. Global config file ~/.config/eca/config.json (or the --config-file path, which replaces the global/local lookup)
  5. Local project config .eca/config.json (per workspace root)
  6. Files listed in extraConfigs, in their listed order

Deep merge means nested maps are merged recursively, while scalars and arrays from a higher-priority source replace (not concatenate) the lower-priority value.

Dynamic string contents#

It's possible to retrieve content of any configs with a string value using the ${key:value} approach, being key:

  • file: ${file:/path/to/my-file} or ${file:../rel-path/to/my-file} to get a file content
  • env: ${env:MY_ENV} to get a system env value with support for default values: ${env:MY_ENV:foo}
  • classpath: ${classpath:path/to/eca/file} to get a file content from ECA's classpath
  • netrc: Support Unix RC credential files
  • cmd: ${cmd:some command} to run a command via the platform shell (bash -c on POSIX, PowerShell on Windows) and use its trimmed stdout — useful for password managers like ${cmd:pass show eca/api-key} or ${cmd:op read op://vault/Item/credential}. On non-zero exit/timeout the value falls back to an empty string and a warning is logged. Since config is re-read frequently, a successful (non-empty) result is cached for 5 minutes per command, so slow commands like token fetchers are not respawned on every read; failures are not cached and are retried on the next read. The command itself cannot contain } (the closing brace ends the placeholder); for commands like awk '{print $1}' …, wrap them in a small script or shell function.

macOS GUI launches

When ECA runs from Finder/Dock (e.g. via ECA Desktop) the inherited PATH is minimal and Homebrew, mise/asdf shims, etc. are not visible. To fix this, on macOS the cmd backend spawns the user's interactive login shell once ($SHELL -ilc '…') and reuses the captured $PATH for subsequent ${cmd:...} resolutions — so anything sourced from .zshrc/.zprofile/.bash_profile is picked up automatically. If the shell query fails or your shell isn't bash/zsh/sh/dash/ksh, ECA falls back to prepending /opt/homebrew/bin, /usr/local/bin and ~/.local/bin. As a last resort, use an absolute path (e.g. ${cmd:/opt/custom/bin/my-tool ...}).

Linux GUI launches can have a similar (less severe) PATH gap, but shell-PATH discovery is currently macOS-only — Linux users with the same problem should use absolute paths in ${cmd:...} for now.

Info

This is supported in markdown configurations of agents and skills as well, giving flexibility to template it.

Schema#

ECA has a config.json schema to validate and autocomplete in your editor.

Default config#

By default ECA consider the following as the base configuration:

    {
      "providers": {
          "openai": {"url": "https://api.openai.com"},
          "anthropic": {"url": "https://api.anthropic.com"},
          "github-copilot": {"url": "https://api.githubcopilot.com"},
          "google": {"url": "https://generativelanguage.googleapis.com/v1beta/openai"},
          "ollama": {"url": "http://localhost:11434"}
      },
      "defaultModel": null, // let ECA decides the default model.
      "netrcFile": null, // search ~/.netrc or ~/_netrc when null.
      "hooks": {},
      "rules" : [],
      "commands" : [],
      "skills": [],
      "disabledTools": [],
      "mcpToolSearch": {
        "deferAllWhenTotalTokensExceedPercentOfContext": null,
        "includePattern": [],
        "excludePattern": []
      },
      "toolCall": {
        "approval": {
          "byDefault": "ask",
          "allow": {"eca__directory_tree": {},
                    "eca__read_file": {},
                    "eca__grep": {},
                    "eca__preview_file_change": {},
                    "eca__editor_diagnostics": {}},
          "ask": {},
          "deny": {}
        },
        "readFile": {
          "maxLines": 2000
        },
        "shellCommand": {
          "summaryMaxLength": 35
        },
        "editorNav": {
          "enabled": true
        }
      },
      "mcpTimeoutSeconds" : 60,
      "mcpKeepAliveSeconds" : 30,
      "lspTimeoutSeconds" : 30,
      "mcpServers" : {},
      "agent": {
        "code": {"prompts": {"chat": "${classpath:prompts/code_agent.md}"},
                 "disabledTools": ["preview_file_change"]},
        "plan": {"prompts": {"chat": "${classpath:prompts/plan_agent.md}"},
                  "disabledTools": ["edit_file", "write_file", "move_file"],
                  "toolCall": {"approval": {"deny": {"eca__shell_command":
                                                     {"argsMatchers": {"command": [".*[12&]?>>?\\s*(?!/dev/null($|\\s))(?!&\\d+($|\\s))\\S+.*",
                                                                                  ".*\\|\\s*(tee|dd|xargs).*",
                                                                                  ".*\\b(sed|awk|perl)\\s+.*-i.*",
                                                                                  ".*\\b(rm|mv|cp|touch|mkdir)\\b.*",
                                                                                  ".*git\\s+(add|commit|push).*",
                                                                                  ".*npm\\s+install.*",
                                                                                  ".*-c\\s+[\"'].*open.*[\"']w[\"'].*",
                                                                                  ".*bash.*-c.*[12&]?>>?\\s*(?!/dev/null($|\\s))(?!&\\d+($|\\s))\\S+.*"]}}}}}}
      },
      "defaultAgent": "code",
      "welcomeMessage" : "Welcome to ECA!\n\nType '/' for commands\n\n",
      "autoCompactPercentage": 85,
      "index" : {
        "ignoreFiles" : [ {
          "type" : "gitignore"
        } ],
        "repoMap": {
          "maxTotalEntries": 800,
          "maxEntriesPerDir": 50
        }
      },
      "prompts": {
        "chat": "${classpath:prompts/code_agent.md}", // default to code agent
        "chatTitle": "${classpath:prompts/title.md}",
        "compact": "${classpath:prompts/compact.md}",
        "init": "${classpath:prompts/init.md}",
        "completion": "${classpath:prompts/inline_completion.md}",
        "rewrite": "${classpath:prompts/rewrite.md}"
      },
      "completion": {
        "model": "openai/gpt-4.1"
      }
    }

Network / Enterprise

For more details about configuring eca with mTLS or custom CA certificates, check network page

Templates#

ECA supports templating in configurable prompts and rules, so instructions can adapt to the current chat context.

Custom agent prompts and rules are rendered with Selmer. Use condition variables when one prompt or rule should behave differently depending on the current chat.

Available variables:

Variable Type Description
isSubagent boolean true when the chat is running as a subagent
workspaceRoots string The current workspace root paths
toolEnabled_<tool-name> boolean true when a tool is enabled, using its exact full name, e.g. toolEnabled_eca__shell_command
Prompt or rule
{% if isSubagent %}
Be concise and return only the final result.
{% else %}
Explain important trade-offs and assumptions.
{% endif %}

{% if toolEnabled_eca__shell_command %}
You can run shell commands to verify your work.
{% endif %}

Current workspace roots: {{ workspaceRoots }}

If a rule renders to an empty string, ECA skips it and does not add an empty rule block to the system prompt.