Skip to content

configuration

dmang-dev edited this page Jul 20, 2026 · 1 revision

Configuration

All eVi state lives under ~/.evi/ (Windows: %USERPROFILE%\.evi\). The primary file is config.toml. First launch writes a default. Hand-edit it when you need to change something; nothing else holds machine-wide state.

Primary file — config.toml

[llm]
backend         = "lmstudio"                  # lmstudio | ollama | llamacpp | openai_compat
base_url        = "http://localhost:1234/v1"  # default per-backend; auto-bumped by `evi models backend`
api_key         = "lm-studio"                 # local servers ignore it; `env:VARNAME` reads the env var
model           = "qwen2.5-7b-instruct"       # current active model
temperature     = 0.7                         # lower (0.3) is friendlier to tool calling
max_tokens      = 4096
request_timeout = 120.0

[comfy]
base_url           = "http://localhost:8188"
default_checkpoint = "sd_xl_base_1.0.safetensors"
default_steps      = 25
default_width      = 1024
default_height     = 1024

[google]                                       # Phase 2 — deferred
client_secrets_path = ""
scopes              = ["https://www.googleapis.com/auth/gmail.readonly"]

[microsoft]                                    # Phase 2 — deferred
client_id = ""
tenant_id = "common"
scopes    = ["Mail.Read", "User.Read"]

[tools]
fs          = true     # read_file / write_file / list_dir
code        = true     # run_python in a subprocess
shell       = false    # not implemented; never default-on
gmail       = false    # Phase 2
outlook     = false    # Phase 2
image       = false    # ComfyUI text2img
memory      = true     # remember / recall / forget / list_memories
subagent    = false    # delegate_explore / delegate_plan
mcp         = false    # MCP-server-provided tools
skills      = true     # list_skills / invoke_skill
web         = false    # web_search / web_fetch (network!)
voice       = false    # speak_text / transcribe_microphone
computer    = false    # never default-on; mouse/keyboard control
transcripts = true     # write session JSONL for dreaming

[auto]
# Categories listed here run without prompting. `computer` is never here.
auto_approve = ["fs", "code", "memory", "skills", "image"]

# Curated destructive-command guard (on by default). Commands like `rm -rf ~`,
# `git reset --hard`, `git push --force`, disk formats, `curl | sh`, secret
# exfil, or `terraform destroy` can never run SILENTLY: a match forces a
# confirmation prompt, and is DENIED when there's no UI to confirm with
# (headless / scheduler / MCP). Overrides yolo, accept_edits and auto_approve.
block_destructive = true
# fnmatch globs matched against the WHOLE command that exempt it from the guard.
destructive_allow = ["*--force-with-lease*"]
# Builtin rule ids to silence without disabling the guard (see evi/shell_guard.py).
destructive_disable_rules = ["git-commit-amend"]

Everyday commands are deliberately not flagged — rm -rf ./build, rm -rf node_modules, git reset --soft, git branch -d, chmod 644, terraform plan, apt remove <app> all run as normal. Only genuinely destructive, hard-to-reverse forms match. An explicit, specific allow rule (one carrying an arg-glob, e.g. allow shell *--force*) is honoured as intent and clears the guard; a broad allow shell / allow shell * does not.

evi config show prints the resolved config (with profile overlay).

evi config path prints the file path.

Online providers — evi models preset

eVi is local-first, but any OpenAI-compatible cloud gateway works via backend = "openai_compat". Named presets make the common ones one command:

evi models preset                         # list providers + whether each key env var is set
evi models preset openrouter -m anthropic/claude-3.5-sonnet
evi models preset openai                  # uses $OPENAI_API_KEY (api=chat; set api=responses for the Responses API)

Presets (openrouter, openai, xai, anthropic, groq, together) all set backend=openai_compat + the provider base_url and, by default, an api_key = "env:<PROVIDER>_API_KEY" reference so your key stays in the environment, not in config.toml. (Pass --api-key to store it inline instead.) The anthropic preset targets Anthropic's OpenAI-compatible endpoint, not the native Messages API.

Claude via your Max/Pro plan — backend = "claude_agent" (no API key)

The claude_agent backend talks to Claude through the local claude CLI (the Claude Agent SDK), authenticating with your Claude subscription login instead of an ANTHROPIC_API_KEY — the sanctioned, no-key way to use your Max/Pro plan from your own tools.

Setup (one time):

npm i -g @anthropic-ai/claude-code   # the `claude` CLI, if not already installed
claude                                # log in on your Max/Pro plan
pip install 'evi-assistant[claude-agent]'

Then add + select it (no URL, no key):

evi backend add claude --kind claude_agent   # or: Settings → Model & Backend → add, kind claude_agent
evi backend use claude --model opus           # models are aliases: opus | sonnet | haiku

The opus / sonnet / haiku aliases resolve to whatever your plan currently serves. Tools work fully — eVi still runs its own tools (with its permissions, checkpoints, and mode-scoped toolsets); the Agent SDK is used only to decide which tool to call. Note this routes through the claude CLI (not an HTTP endpoint), so it's local-machine-only and needs the CLI logged in; if the SDK/CLI is missing, eVi says so the moment you select the backend.

OpenAI Codex via your ChatGPT plan — backend = "codex" (no API key)

The codex backend is the OpenAI counterpart: it talks to the local codex CLI authenticated by codex login (ChatGPT Plus/Pro/Business), no OPENAI_API_KEY.

npm i -g @openai/codex   # the `codex` CLI (or: brew install --cask codex)
codex login              # browser OAuth against your ChatGPT plan
evi backend add codex --kind codex     # or Settings → Model & Backend → kind codex
evi backend use codex --model gpt-5-codex   # models: gpt-5-codex | gpt-5

Under the hood eVi runs codex exec --json (read-only) and streams the answer. Difference from claude_agent: Codex is an autonomous agent that runs its own tools in its sandbox, so it's a chat / delegate provider — eVi's tools don't route through it, and it can't edit files during a chat turn. Both backends are built on the same shared CLI-agent shim (evi/llm/cli_agent.py), so they behave consistently and adding further subscription-login CLIs is cheap.

Google Gemini via the free login — backend = "gemini" (no API key)

The gemini backend uses the local gemini CLI: signing in with a Google account gives a free tier (~1000 requests/day) with no GEMINI_API_KEY.

npm i -g @google/gemini-cli   # the `gemini` CLI
gemini                        # run once and pick a Google login
evi backend add gemini --kind gemini        # or Settings → Model & Backend → kind gemini
evi backend use gemini --model gemini-2.5-flash   # models: gemini-2.5-pro | gemini-2.5-flash

eVi runs gemini -p … -o json and streams the reply. Like codex it's a chat / delegate provider (Gemini drives its own tools; eVi's tools don't route through it), built on the same shared shim.

Sourcegraph Amp via your subscription — backend = "amp" (no API key)

The amp backend uses the local amp CLI, authenticated by amp login (an Amp subscription / credit balance) or an AMP_API_KEY access token — not a per-token model API key.

npm i -g @sourcegraph/amp   # the `amp` CLI
amp login                   # Amp subscription (or: set AMP_API_KEY)
evi backend add amp --kind amp             # or Settings → Model & Backend → kind amp
evi backend use amp --model medium         # modes: medium | low | high

eVi runs amp -x --stream-json and streams the reply. Amp selects its model by agent mode (low/medium/high), not a model id. Like codex/gemini it's a chat / delegate provider, but note Amp is autonomous and can use tools (including file edits) per your configured amp permissions — restrict it there if you want a read-only chat. eVi won't start Amp unauthenticated (its login flow would block), and it bounds each turn with a timeout as a backstop.

Qwen Code via the free Qwen login — backend = "qwen" (no API key)

The qwen backend uses Qwen Code (Alibaba's gemini-cli fork): sign in free with a qwen.ai account (a generous free tier, ~2000 requests/day) — no API key.

npm i -g @qwen-code/qwen-code   # the `qwen` CLI
qwen                            # run once and pick 'Qwen' to sign in (free OAuth)
evi backend add qwen --kind qwen            # or Settings → Model & Backend → kind qwen
evi backend use qwen --model qwen3-coder-plus   # models: qwen3-coder-plus | qwen3-coder-flash

eVi runs qwen -p … -o json (Claude-Code-style events) and streams the reply — a chat / delegate provider like gemini, on the same shared shim.

GitHub Copilot via your subscription — backend = "copilot" (no API key)

The copilot backend uses the local copilot CLI (@github/copilot), authenticated by your GitHub Copilot subscription (copilot login) — no separate model API key.

npm i -g @github/copilot   # the `copilot` CLI
copilot login              # GitHub Copilot subscription
evi backend add copilot --kind copilot          # or Settings → Model & Backend → kind copilot
evi backend use copilot --model auto            # models: auto | claude-sonnet-4.5 | gpt-5

eVi runs copilot -p … --output-format text -s and streams the reply. --model auto lets Copilot pick; the exact set depends on your plan. A chat / delegate provider; unapproved tool calls are auto-denied in non-interactive mode, so a chat turn stays answer-only.

The CLI-agent backends at a glance

kind CLI Auth (no API key) Tools
claude_agent claude Claude Max/Pro login eVi drives tools (full parity)
codex codex ChatGPT Plus/Pro login delegate (Codex's own, read-only)
gemini gemini Google free login delegate (Gemini's own)
amp amp Amp subscription (amp login / AMP_API_KEY) delegate (Amp's own, per amp permissions)
qwen qwen Qwen free OAuth login delegate (Qwen's own)
copilot copilot GitHub Copilot login delegate (Copilot's own, auto-denied)

All six are local-machine-only (they shell out to a CLI, so they don't work in the packaged desktop sidecar) and report clearly if their CLI isn't installed.

Profiles — ~/.evi/profiles/<name>.toml

Partial overlay merged on top of config.toml. Activated by env var EVI_PROFILE=<name> or --profile <name> / -p <name> on any CLI invocation.

# ~/.evi/profiles/home.toml
[llm]
backend  = "openai_compat"
base_url = "http://ai-server.local:8000/v1"
model    = "qwen2.5:32b"
evi profile add away --backend lmstudio --model llama3.2:1b-instruct-q4_K_M
evi profile list
evi --profile home chat

Profile merge is deep for tables (dicts) and replacing for lists. So a profile that overrides [microsoft] scopes replaces it wholesale rather than appending.

Memory — ~/.evi/memory/

One markdown file per fact:

~/.evi/memory/
  INDEX.md             auto-regenerated; you can read but don't edit
  preferences.md       arbitrary name; first non-empty line becomes the summary
  project_paths.md
  .attic/              soft-deleted entries; safe to remove if you want

The Agent.memory.format_for_prompt() block shows up in every system prompt. The model uses the remember(name, content), recall(name), and forget(name) tools to manage it. forget is soft-delete — it moves to .attic/. The dreaming engine relies on this to avoid losing data on bad consolidations.

Caps: 64 KB per entry, names must match [A-Za-z0-9_-]{1,64}.

Skills — ~/.evi/skills/<name>/SKILL.md

~/.evi/skills/
  summarize/
    SKILL.md          required — instructions + optional YAML frontmatter
    example-input.txt  optional assets (loadable by the model with read_file)
---
name: summarize
description: Boil long text down to 3 bullets.
---

# Steps
1. Identify the topic.
2. Pull the three highest-impact points.
3. Format as Markdown bullets ≤ 12 words each.

The frontmatter description shows up in the system prompt's ## Available skills block. The model calls invoke_skill(name) to load the full body when it decides the skill applies.

Slash commands — ~/.evi/commands/<name>.md

User-defined prompt templates. Type /<name> args in the chat REPL or web UI and the file's body is sent as the user turn with {args} substituted.

# ~/.evi/commands/commit.md
Run `git diff` to see what changed, then propose a conventional-commits
style message under 70 chars. Args: {args}

Built-in slash commands (handled in code, not files): /help /reset /exit /tools /model /goal /plan /auto.

Hooks — ~/.evi/hooks.toml

[[before_tool_call]]
name             = "audit"
match            = "*"                  # glob over tool names (fs.*, write_file, etc.)
command          = ["bash", "-c", "echo $EVI_HOOK_TOOL >> ~/.evi/logs/tools.log"]
timeout          = 5

[[before_tool_call]]
name             = "no-system-writes"
match            = "write_file"
command          = ["bash", "-c", 'echo "$EVI_HOOK_ARGS_JSON" | grep -qv "/etc/"']
veto_on_nonzero  = true                 # non-zero exit blocks the tool

[[after_tool_call]]
name    = "notify"
match   = "generate_image"
command = ["notify-send", "Image ready"]

[[after_tool_call]]
name    = "webhook"
match   = "*"
url     = "https://example.com/evi-hook"   # POST instead of spawning a command

Env vars set in the child process (command hooks):

  • EVI_HOOK_EVENTbefore_tool_call or after_tool_call
  • EVI_HOOK_TOOL — fully-qualified tool name
  • EVI_HOOK_ARGS_JSON — JSON of the call arguments
  • EVI_HOOK_RESULT — tool output (after-hooks only, capped at 4 KB)

A hook uses either command (argv, spawned) or url (HTTP POST of {event, tool, args_json, result}). For a url hook a 2xx response means success; any other status becomes the exit code, so veto_on_nonzero still gates the call.

Besides the tool events, hooks fire on lifecycle events (use match = "*"):

[[user_prompt_submit]]   # before each turn — veto blocks the prompt
name = "no-secrets"
command = ["python3", "/path/check_prompt.py"]   # prompt is in EVI_HOOK_ARGS_JSON
veto_on_nonzero = true

[[before_compact]]       # before history compaction — veto keeps it intact
[[stop]]                 # after a turn completes (notification; never blocks)

Keybindings — ~/.evi/keybindings.toml

Map a key to a slash command in the interactive chat REPL — pressing it replaces the line with that command and submits it.

[keybindings]
"c-t"      = "/tools"      # Ctrl-T
"f2"       = "/model"      # F2
"escape g" = "/goal"       # Esc then g (a two-key sequence)

Keys use prompt_toolkit names (c-t, f2, escape, …); a space-separated value is a multi-key sequence. Terminal essentials (c-c, c-d, tab, enter) are reserved and silently ignored, and an unknown key name is skipped without breaking the others.

Voice / TTS — [voice] in config.toml

[voice]
engine       = "system"   # system | coqui | f5 | piper
model        = ""         # engine-specific: Coqui XTTS id, or a Piper voice .onnx path
clone_sample = ""         # reference WAV for the cloning engines (coqui / f5)
language     = "en"
  • system — zero-dep platform voice (Windows SAPI / macOS say / espeak).
  • coqui — Coqui XTTS v2; multilingual, clones a voice from clone_sample.
  • f5 — F5-TTS; fast zero-shot cloning (uses its f5-tts_infer-cli).
  • piper — lightweight local neural voices (set model to a .onnx); no cloning.

The neural engines are optional heavyweight installs; eVi lazy-imports them and falls back to a clear error if the deps/binaries aren't present. evi voice engines shows which are installed and which is active; switch the engine in the desktop Settings → Voice screen or by editing [voice].

Workflows — ~/.evi/workflows/<name>.toml

Where a recipe is a sequence of turns through one conversation, a workflow orchestrates independent steps — each its own headless agent — with parallel fan-out and variable interpolation.

name = "research"
description = "Plan, research two angles in parallel, then synthesize."

[vars]
topic = "local-first AI"

[[steps]]
id = "plan"
prompt = "Outline an approach to research {topic}."

[[steps]]
id = "pros"
parallel = true
prompt = "List the upsides of {topic} given this plan:\n{plan}"

[[steps]]
id = "cons"
parallel = true
prompt = "List the downsides of {topic} given this plan:\n{plan}"

[[steps]]
id = "synth"
prompt = "Synthesize a balanced take.\nUpsides:\n{pros}\nDownsides:\n{cons}"
  • Steps run in file order; a contiguous run of parallel = true steps runs concurrently. A following sequential step is the natural fan-in point.
  • Prompts interpolate workflow [vars] and earlier step outputs by id — {topic}, {plan}, … (escape literal braces as {{ / }}).
  • Each step is an unattended (auto-approved) headless agent; set mode on a step for a tool preset (chat/cowork/code).

evi workflow new <name> scaffolds one; evi workflow run <name> --var topic=… runs it (--json for machine output). The desktop 🗂 Dispatch panel lists and launches workflows and shows every live session.

Plugins — ~/.evi/plugins/<name>/ + marketplace

A plugin bundles any of: commands/, skills/, hooks.toml, mcp.json, and agents.toml (subagent profiles, used via the delegate tool — see evi agents). Install from a directory or git URL with evi plugin add, or by name from the marketplace index:

// ~/.evi/marketplace.json
{
  "plugins": [
    { "name": "git-helpers",
      "source": "https://github.com/you/evi-git-helpers.git",
      "description": "Handy git slash commands",
      "tags": ["git"] }
  ]
}
# config.toml — extra remote index files merged with the local one
[plugins]
index_urls = ["https://example.com/evi-plugins.json"]

evi plugin search [query] lists matches; evi plugin install <name> resolves the name through the index and installs its source. evi plugin index init / evi plugin index add <name> <source> manage the local index.

Federation — ~/.evi/peers.json

Delegate a task to a trusted peer eVi (e.g. a GPU box):

[ { "name": "gpu", "url": "http://gpu-box:8473", "token": "<peer web token>" } ]

evi peer run gpu "summarise this repo" (or the delegate_peer tool, category federation, off by default) POSTs to the peer's /api/federate. The peer must opt in with [federation] serve = true; it runs the task non-interactively (tools not auto-approved are denied).

A2A (Agent2Agent) — interop with any agent

Federation is eVi's private fast path to your own eVis. A2A (a2a-protocol.org, a Linux Foundation standard) is the interop path — it lets eVi talk to any vendor's agent that speaks A2A, and lets such agents call eVi. No extra dependency (hand-rolled against the v0.3/v1.0 wire shapes).

Discovery — always on, public. eVi serves a spec-compliant Agent Card at GET /.well-known/agent-card.json (auth-exempt). It carries the standard fields (protocolVersion, capabilities, skills, securitySchemes) plus eVi's model capability flags under an x-evi extension, so capability-aware clients can route by modality.

Serve — opt in. Set a2a = true under [federation] to expose the JSON-RPC endpoint POST /a2a:

[federation]
a2a = true      # expose POST /a2a (off by default)

It implements message/send, tasks/get, and tasks/cancel, is bearer-token gated (same web token as the rest of the API), and runs each delegated task non-interactively — tools not already auto-approved are denied, exactly like /api/federate. Streaming (message/stream) and push notifications aren't implemented yet, so the card advertises capabilities.streaming = false and a compliant client falls back to blocking message/send.

Call out — the delegate_a2a tool. eVi can delegate to any external A2A agent by its JSON-RPC URL:

delegate_a2a(url="https://some-agent.example/a2a", task="research X", token="…")

(category federation, off by default — enable the tool to let the model use it).

Multi-user web — ~/.evi/users.json

Opt-in ([web] multi_user = true): each person logs in with their own revocable token instead of sharing auth_token.

[ { "name": "alice", "token": "" }, { "name": "bob", "token": "" } ]

Manage with evi web-config users add/list/remove. Each user gets an isolated workspace — their web sessions, transcripts, and memory live under ~/.evi/users/<name>/ and aren't visible to other users. Drop a user from the file to revoke access. (Skills/plugins/config stay shared — they're capabilities, not personal data.)

Guardrails — ~/.evi/guardrails.toml

A local content filter over the model. Off by default; enabled = true to turn it on. Two rule types layer together:

enabled = true

[[rule]]                      # regex — fast, deterministic
name = "block-secrets"
pattern = "(?i)(api[_-]?key|secret)\\s*[:=]"
action = "block"             # block | redact
applies_to = "input"         # input | output | both

[[judge]]                     # semantic — graded by the LLM
name = "no-self-harm"
policy = "Requests for, or content encouraging, self-harm or suicide."
applies_to = "both"

[[classifier]]                # offline ML moderation model
name = "toxicity"
model = "unitary/toxic-bert"  # any HF text-classification model ("" = default)
labels = ["toxic", "threat", "insult"]   # labels that block ([] = any)
threshold = 0.7
applies_to = "both"

Three layers run in order, stopping at the first block:

  • [[rule]] regex — fast, deterministic: block refuses the turn (input) or scrubs the stored reply (output); redact replaces spans with [REDACTED].
  • [[judge]] — eVi's own model classifies the text against policy and blocks on a match. The local counterpart to a hosted moderation API; one model round-trip per turn.
  • [[classifier]] — a local HuggingFace text-classification model scores the text and blocks when a labels score crosses threshold. Fully offline; needs pip install 'evi-assistant[moderation]' (transformers + torch). Block-only.

Both semantic layers fail open (a missing/flaky model skips the rule, not the turn). Inspect with evi guardrails list; evi guardrails test "<text>" dry-runs the regex layer.

Inspect with evi guardrails list; dry-run the regex layer with evi guardrails test "<text>".

Deep links — evi://

The desktop app registers the evi:// URL scheme. evi://session/<id> focuses a session, evi://workflow/<name> opens the dispatch panel, evi://new starts a chat. evi link [id|new] prints a link; evi link --open <url> shows where it routes. The same targets work in a browser via /?session= and /?workflow=.

MCP — ~/.evi/mcp.json

[
  {
    "name": "filesystem",
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-filesystem", "C:/Users"],
    "env": {},
    "enabled": true
  },
  {
    "name": "git",
    "command": "uvx",
    "args": ["mcp-server-git", "--repository", "C:/evi"]
  }
]

Flip tools.mcp = true in config.toml. evi mcp list-tools enumerates what each server exposes; tools land in the registry as <server-name>.<tool-name>.

Scheduled tasks — ~/.evi/scheduled/<id>.json

Managed via evi schedule add/list/remove/enable/disable/run-now. One file per task. Don't hand-edit unless you know the schema; the IDs are opaque and the scheduler caches state.

evi schedule add --name "morning brief" --cron "0 8 * * *" \
                 --prompt "Summarize my overnight email."
evi scheduler          # foreground daemon, or just run `evi web`

Where things write logs

What Where
Dream audit ~/.evi/logs/dreams/<stamp>.log
Scheduled task runs ~/.evi/logs/scheduled/<id>_<stamp>.log
User-defined hooks Wherever your hook commands write
Session transcripts ~/.evi/transcripts/<YYYY-MM-DD>/<session>.jsonl
ComfyUI images ~/.evi/images/
Screenshots ~/.evi/screenshots/
HF model downloads ~/.evi/models/<repo-flat>/

Environment variables

Var Purpose
EVI_HOME Override ~/.evi/ location entirely
EVI_PROFILE Active profile name (same as --profile)
EVI_PYTHON Tauri desktop: Python interpreter to spawn (default py -3.13)
EVI_REPO_ROOT Tauri desktop: pin the repo root rather than auto-detecting
EVI_REMOTE_URL Tauri desktop: thin-client mode — skip spawn, navigate to URL

Generated from docs/configuration.md — edit there, not here.

Clone this wiki locally