Skip to content

Configuration#

Check all available configs and its default values here.

Also, check Examples which contains examples of users ECA configs.

Ways to configure#

There are multiples ways to configure ECA:

Convenient for users and multiple projects

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

Convenient for users

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

Convenient for editors

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

"initializationOptions": {
  "defaultBehavior": "plan"
}

Via env var during server process spawn:

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

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
  • classpath: ${classpath:path/to/eca/file} to get a file content from ECA's classpath
  • netrc: Support Unix RC credential files

Providers / Models#

For providers and models configuration check the dedicated models section.

Tools#

MCP#

For MCP servers configuration, use the mcpServers config, examples:

~/.config/eca/config.json
{
  "mcpServers": {
    "memory": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-memory"],
      // optional
      "env": {"FOO": "bar"}
    }
  }
}
~/.config/eca/config.json
{
  "mcpServers": {
    "cool-mcp": {
      "url": "https://my-remote-mcp.com/mcp"
    }
  }
}

Approval / permissions#

By default, ECA asks to call any non read-only tool (default here), but that can easily be configured in several ways via the toolCall.approval config.

You can configure the default behavior with byDefault and/or configure a tool in ask, allow or deny configs.

Check some examples:

~/.config/eca/config.json
{
  "toolCall": {
    "approval": {
      "byDefault": "allow"
    }
  }
}
~/.config/eca/config.json
{
  "toolCall": {
    "approval": {
      "byDefault": "allow",
      "ask": {
        "eca_editfile": {},
        "my-mcp__my_tool": {}
      }
    }
  }
}
~/.config/eca/config.json
{
  "toolCall": {
    "approval": {
      // "byDefault": "ask", not needed as it's eca default
      "allow": {
        "eca": {},
        "my-mcp": {}
      }
    }
  }
}

argsMatchers is a map of argument name by list of java regex.

~/.config/eca/config.json
{
  "toolCall": {
    "approval": {
      "byDefault": "allow",
      "ask": {
        "shell_command": {"argsMatchers": {"command": [".*rm.*",
                                                           ".*mv.*"]}}
      }
    }
  }
}
~/.config/eca/config.json
{
  "toolCall": {
    "approval": {
      "byDefault": "allow",
      "deny": {
        "shell_command": {"argsMatchers": {"command": [".*rm.*",
                                                           ".*mv.*"]}}
      }
    }
  }
}

Also check the plan behavior which is safer.

The manualApproval setting was deprecated and replaced by the approval one without breaking changes

File Reading#

You can configure the maximum number of lines returned by the eca__read_file tool:

~/.config/eca/config.json
{
  "toolCall": {
    "readFile": {
      "maxLines": 1000
    }
  }
}

Default: 2000 lines

Custom Tools#

You can define your own command-line tools that the LLM can use. These are configured via the customTools key in your config.json.

The customTools value is an object where each key is the name of your tool. Each tool definition has the following properties:

  • description: A clear description of what the tool does. This is crucial for the LLM to decide when to use it.
  • command: An string representing the command and its static arguments.
  • schema: An object that defines the parameters the LLM can provide.
    • properties: An object where each key is an argument name.
    • required: An array of required argument names.

Placeholders in the format {{argument_name}} within the command string will be replaced by the values provided by the LLM.

~/.config/eca/config.json
{
  "customTools": {
    "web-search": {
      "description": "Fetches the content of a URL and returns it in Markdown format.",
      "command": "trafilatura --output-format=markdown -u {{url}}",
      "schema": {
        "properties": {
          "url": {
            "type": "string",
            "description": "The URL to fetch content from."
          }
        },
        "required": ["url"]
      }
    }
  }
}
~/.config/eca/config.json
{
  "customTools": {
    "file-search": {
      "description": "${file:tools/my-tool.md}",
      "command": "find {{directory}} -name {{pattern}}",
      "schema": {
        "properties": {
          "directory": {
            "type": "string",
            "description": "The directory to start the search from."
          },
          "pattern": {
            "type": "string",
            "description": "The search pattern for the filename (e.g., '*.clj')."
          }
        },
        "required": ["directory", "pattern"]
      }
    }
  }
}

Custom command prompts#

You can configure custom command prompts for project, global or via commands config pointing to the path of the commands. Prompts can use variables like $ARGUMENTS, $ARG1, ARG2, to replace in the prompt during command call.

You can configure in multiple different ways:

A .eca/commands folder from the workspace root containing .md files with the custom prompt.

.eca/commands/check-performance.md
Check for performance issues in $ARG1 and optimize if needed.

ECA will make available a /check-performance command after creating that file.

A $XDG_CONFIG_HOME/eca/commands or ~/.config/eca/commands folder containing .md files with the custom command prompt.

~/.config/eca/commands/check-performance.md
Check for performance issues in $ARG1 and optimize if needed.

ECA will make available a /check-performance command after creating that file.

Just add to your config the commands pointing to .md files that will be searched from the workspace root if not an absolute path:

~/.config/eca/config.json
{
  "commands": [{"path": "my-custom-prompt.md"}]
}

ECA will make available a /my-custom-prompt command after creating that file.

Rules#

Rules are contexts that are passed to the LLM during a prompt and are useful to tune prompts or LLM behavior. Rules are text files (typically .md, but any format works):

There are 3 possible ways to configure rules following this order of priority:

A .eca/rules folder from the workspace root containing .md files with the rules.

.eca/rules/talk_funny.md
- Talk funny like Mickey!

A $XDG_CONFIG_HOME/eca/rules or ~/.config/eca/rules folder containing .md files with the rules.

~/.config/eca/rules/talk_funny.md
- Talk funny like Mickey!

Just add toyour config the :rules pointing to .md files that will be searched from the workspace root if not an absolute path:

~/.config/eca/config.json
{
  "rules": [{"path": "my-rule.md"}]
}

Behaviors / prompts#

ECA allows to totally customize the prompt sent to LLM via the behavior config, allowing to have multiple behaviors for different tasks or workflows.

~/.config/eca/config.json
{
  "behavior": {
    "my-behavior": {
      "systemPrompt": "${file:/path/to/my-behavior-prompt.md}"
    }
  }
}

Hooks#

Hooks are shell actions that run before or after specific events, useful for notifications, injecting context, modifying inputs, or blocking tool calls.

Hook Types#

Type When Can Modify
sessionStart Server initialized -
sessionEnd Server shutting down -
chatStart New chat or resumed chat Can inject additionalContext
chatEnd Chat deleted -
preRequest Before prompt sent to LLM Can rewrite prompt, inject context, stop request
postRequest After prompt finished -
preToolCall Before tool execution Can modify args, override approval, reject
postToolCall After tool execution Can inject context for next LLM turn

Hook Options#

  • matcher: Regex for server__tool-name, only for *ToolCall hooks.
  • visible: Show hook execution in chat (default: true).
  • runOnError: For postToolCall, run even if tool errored (default: false).

Execution Details#

  • Order: Alphabetical by key. Prompt rewrites chain; argument updates merge (last wins).
  • Conflict: Any rejection (deny or exit 2) blocks the call immediately.
  • Timeout: Actions time out after 30s unless "timeout": ms is set.

Input / Output#

Hooks receive JSON via stdin with event data (top-level keys snake_case, nested data preserves case). Common fields:

  • All hooks: hook_name, hook_type, workspaces, db_cache_path
  • Chat hooks add: chat_id, behavior
  • Tool hooks add: tool_name, server, tool_input, approval (pre) or tool_response, error (post)
  • chatStart adds: resumed (boolean)

Hooks can output JSON to control behavior:

{
  "additionalContext": "Extra context for LLM",  // injected as XML block
  "replacedPrompt": "New prompt text",           // preRequest only
  "updatedInput": {"key": "value"},              // preToolCall: merge into tool args
  "approval": "allow" | "ask" | "deny",          // preToolCall: override approval
  "continue": false,                             // stop processing (with optional stopReason)
  "stopReason": "Why stopped",
  "suppressOutput": true                         // hide hook output from chat
}

Plain text output (non-JSON) is treated as additionalContext.

To reject a tool call, either output {"approval": "deny"} or exit with code 2.

Examples#

~/.config/eca/config.json
{
  "hooks": {
    "notify-me": {
      "type": "postRequest",
      "visible": false,
      "actions": [{"type": "shell", "shell": "notify-send 'Prompt finished!'"}]
    }
  }
}
~/.config/eca/hooks/my-hook.sh
jq -e '.approval == "ask"' > /dev/null && canberra-gtk-play -i complete
~/.config/eca/config.json
{
  "hooks": {
    "notify-me": {
      "type": "preToolCall",
      "visible": false,
      "actions": [
        {
          "type": "shell",
          "file": "hooks/my-hook.sh"
        }
      ]
    }
  }
}
~/.config/eca/config.json
{
  "hooks": {
    "load-context": {
      "type": "chatStart",
      "actions": [{
        "type": "shell",
        "shell": "echo '{\"additionalContext\": \"Today is '$(date +%Y-%m-%d)'\"}'"
      }]
    }
  }
}
~/.config/eca/config.json
{
  "hooks": {
    "add-prefix": {
      "type": "preRequest",
      "actions": [{
        "type": "shell",
        "shell": "jq -c '{replacedPrompt: (\"[IMPORTANT] \" + .prompt)}'"
      }]
    }
  }
}
~/.config/eca/config.json
{
  "hooks": {
    "block-rm": {
      "type": "preToolCall",
      "matcher": "eca__shell_command",
      "actions": [{
        "type": "shell",
        "shell": "if jq -e '.tool_input.command | test(\"rm -rf\")' > /dev/null; then echo '{\"approval\":\"deny\",\"additionalContext\":\"Dangerous command blocked\"}'; fi"
      }]
    }
  }
}
~/.config/eca/config.json
{
  "hooks": {
    "force-recursive": {
      "type": "preToolCall",
      "matcher": "eca__directory_tree",
      "actions": [{
        "type": "shell",
        "shell": "echo '{\"updatedInput\": {\"max_depth\": 3}}'"
      }]
    }
  }
}
~/.config/eca/config.json
{
  "hooks": {
    "my-hook": {
      "type": "preToolCall",
      "actions": [{"type": "shell", "file": "~/.config/eca/hooks/check-tool.sh"}]
    }
  }
}

Completion#

You can configure which model and system prompt ECA will use during its inline completion:

~/.config/eca/config.json
{
  "completion": {
    "model": "github-copilot/gpt-4.1",
    "systemPrompt": "${file:/path/to/my-prompt.md}"
  }
}

Rewrite#

Configure the model and system prompt used for ECA's rewrite feature via the rewrite config. By default, ECA follows the same model selection as chat unless overwritten:

~/.config/eca/config.json
{
  "rewrite": {
    "model": "github-copilot/gpt-4.1",
    "systemPrompt": "${file:/path/to/my-prompt.md}"
  }
}

Opentelemetry integration#

To configure, add your OTLP collector config via :otlp map following otlp auto-configure settings. Example:

~/.config/eca/config.json
{
  "otlp": {
    "otel.exporter.otlp.metrics.protocol": "http/protobuf",
    "otel.exporter.otlp.metrics.endpoint": "https://my-otlp-endpoint.com/foo",
    "otel.exporter.otlp.headers": "Authorization=Bearer 123456"
  }
}

All configs#

interface Config {
    providers?: {[key: string]: {
        api?: 'openai-responses' | 'openai-chat' | 'anthropic';
        url?: string;
        key?: string; // when provider supports api key.
        keyRc?: string; // credential file lookup in format [login@]machine[:port]
        completionUrlRelativePath?: string;
        thinkTagStart?: string;
        thinkTagEnd?: string;
        models: {[key: string]: {
          modelName?: string;
          extraPayload?: {[key: string]: any}
        }};
    }};
    defaultModel?: string;
    hooks?: {[key: string]: {
            type: 'sessionStart' | 'sessionEnd' | 'chatStart' | 'chatEnd' |
                  'preRequest' | 'postRequest' | 'preToolCall' | 'postToolCall';
            matcher?: string; // regex for server__tool-name, only *ToolCall hooks
            visible?: boolean;
            runOnError?: boolean; // postToolCall only
            actions: {
                type: 'shell';
                shell?: string; // inline script
                file?: string;  // path to script file
                timeout?: number; // ms, default 30000
            }[];
        };
    };
    rules?: [{path: string;}];
    commands?: [{path: string;}];
    behavior?: {[key: string]: {
        systemPrompt?: string;
        defaultModel?: string;
        disabledTools?: string[];
        toolCall?: {
            approval?: {
                byDefault?: 'ask' | 'allow' | 'deny';
                allow?: {[key: string]: {argsMatchers?: {[key: string]: string[]}}};
                ask?: {[key: string]: {argsMatchers?: {[key: string]: string[]}}};
                deny?: {[key: string]: {argsMatchers?: {[key: string]: string[]}}};
            };
        };
    }};
    customTools?: {[key: string]: {
        description: string;
        command: string;
        schema: {
            properties: {[key: string]: {
                type: string;
                description: string;
            }};
            required: string[];
        };
    }};
    disabledTools?: string[],
    toolCall?: {
      approval?: {
        byDefault: 'ask' | 'allow';
        allow?: {{key: string}: {argsMatchers?: {{[key]: string}: string[]}}},
        ask?: {{key: string}: {argsMatchers?: {{[key]: string}: string[]}}},
        deny?: {{key: string}: {argsMatchers?: {{[key]: string}: string[]}}},
      };
      readFile?: {
        maxLines?: number;
      };
      shellCommand?: {
        summaryMaxLength?: number,
      };
    };
    mcpTimeoutSeconds?: number;
    lspTimeoutSeconds?: number;
    mcpServers?: {[key: string]: {
        url?: string; // for remote http-stremable servers
        command?: string; // for stdio servers
        args?: string[];
        disabled?: boolean;
    }};
    defaultBehavior?: string;
    welcomeMessage?: string;
    compactPromptFile?: string;
    index?: {
        ignoreFiles: [{
            type: string;
        }];
        repoMap?: {
            maxTotalEntries?: number;
            maxEntriesPerDir?: number;
        };
    };
    completion?: {
        model?: string;
        systemPrompt?: string;
    };
    rewrite?: {
        model?: string;
        systemPrompt?: string;
    };
    otlp?: {[key: string]: string};
    netrcFile?: string;
}
{
  "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" : [],
  "disabledTools": [],
  "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": 30,
    },
  },
  "mcpTimeoutSeconds" : 60,
  "lspTimeoutSeconds" : 30,
  "mcpServers" : {},
  "behavior" {
    "agent": {"systemPrompt": "${classpath:prompts/agent_behavior.md}",
              "disabledTools": ["preview_file_change"]},
    "plan": {"systemPrompt": "${classpath:prompts/plan_behavior.md}",
              "disabledTools": ["edit_file", "write_file", "move_file"],
              "toolCall": {"approval": {"deny": {"eca__shell_command":
                                                 {"argsMatchers": {"command" [".*>.*",
                                                                              ".*\\|\\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.*>.*"]}}}}}}
  }
  "defaultBehavior": "agent",
  "welcomeMessage" : "Welcome to ECA!\n\nType '/' for commands\n\n",
  "compactPromptFile": "prompts/compact.md",
  "index" : {
    "ignoreFiles" : [ {
      "type" : "gitignore"
    } ],
    "repoMap": {
      "maxTotalEntries": 800,
      "maxEntriesPerDir": 50
    }
  },
  "completion": {
    "model": "openai/gpt-4.1",
    "systemPrompt": "${classpath:prompts/inline_completion.md}"
  },
  "rewrite": {
    "systemPrompt": "${classpath:prompts/rewrite.md}"
  }
}