No description
  • Go 93.5%
  • JavaScript 5.1%
  • CSS 0.7%
  • Shell 0.6%
  • HTML 0.1%
Find a file
eko 19091973a4
Some checks failed
ci / gate (push) Has been cancelled
Release v1.4.0
2026-09-20 04:52:36 -04:00
.agents/skills/orchestrate fix(daemon): worker fan-out, delivery wake, and session-log durability 2026-09-17 02:36:37 -04:00
.github/workflows ci: add the review's minimum gate as a workflow (R2-40) 2026-08-26 21:53:24 -04:00
cmd feat(orchestrate): swarm worker orchestration polish 2026-09-16 17:10:05 -04:00
demo feat(demo): record a real conversation for the README, replayed via canned tools 2026-08-01 00:01:13 -04:00
docs fix(tui): make live tool streaming render reliably 2026-09-20 04:51:31 -04:00
internal Release v1.4.0 2026-09-20 04:52:36 -04:00
probe fix(tui): make live tool streaming render reliably 2026-09-20 04:51:31 -04:00
testdata fix(J1.1): inline images never reached the terminal 2026-08-01 20:46:47 -04:00
.gitignore feat(orchestrate): swarm worker orchestration polish 2026-09-16 17:10:05 -04:00
go.mod feat(tui): /tailscale — one-command web over the tailnet 2026-09-17 01:00:07 -04:00
go.sum feat(tui): /tailscale — one-command web over the tailnet 2026-09-17 01:00:07 -04:00
install.sh harden: tighten runtime safety and persistence 2026-08-20 05:24:02 -04:00
LICENSE docs: rewrite README, add GPL-3.0 2026-07-31 04:56:25 -04:00
README.md docs(plan): P3+P4 checked off with the tests and probes that verified them; README documents the live transcript + live panel 2026-09-20 03:55:02 -04:00

evilcode

License Go Built with Charm Ollama Linux

A note before you use it: evilcode was built with AI. I have extensively cross-verified it against other open-source coding agents wherever the features overlap, but it is still an active personal project and should be treated that way. Pull requests and feature requests are welcome.

evilcode is a coding agent for the terminal. It gives a model a real tool set — files, shell commands, git, LSP, memory, and more — and puts the conversation in a fast, keyboard-first TUI.

I originally built evilcode for myself, so it has opinions. Some of them may not match how you would design a coding agent, and the project is not especially extensible yet. That is part of the current state of the project, not a promise that every edge case has been solved. If you want it to work differently, open an issue or send a PR.

evilcode auditing a real codebase for panic-shaped bugs

What it feels like

evilcode is single-user and local by default. There is no account, telemetry, or hosted workspace. A small per-user daemon owns the live sessions, while terminal windows are just clients that can connect, disconnect, and reconnect. Conversations are stored as JSONL files on your machine so a session can be resumed later.

The main command opens the TUI and starts the daemon when needed:

evilcode

From there you can type a prompt, resume a session, inspect files, run commands, review diffs, switch models, and keep working while tools run in the background.

A session is created by the first prompt, not by opening the TUI: evilcode opened and quit without prompting leaves nothing behind. Quitting with Ctrl+C twice (or /quit) also stops the daemon when it holds nothing else — no other live session, no turn in flight, no other window — which is the same as evilcode serve -stop by hand.

Install

The quickest install is the latest Linux/amd64 release:

curl -fsSL https://evileko.dev/evilcode | sh

This installs evilcode and an ec symlink in ~/.local/bin. The installer prints what it is doing and warns if that directory is not on your PATH.

Already installed? Update in place with:

evilcode update

The installer also offers update, reinstall, removal, and config reset options instead of silently replacing an existing install.

Build from source

You need Go 1.26 or newer:

git clone https://git.evileko.dev/evileko/evilcode
cd evilcode
go build -o evilcode ./cmd/evilcode
ln -sf "$PWD/evilcode" ~/.local/bin/evilcode
ln -sf "$PWD/evilcode" ~/.local/bin/ec

Shell completions

evilcode completions zsh  > ~/.zfunc/_evilcode
evilcode completions bash > /etc/bash_completion.d/evilcode
evilcode completions fish > ~/.config/fish/completions/evilcode.fish

Usage

evilcode                              # open the TUI
evilcode run "fix the parser"         # submit a prompt and return
evilcode run --wait "explain this"    # submit and stream the answer
evilcode serve                        # run the daemon in the foreground
evilcode serve -web                   # also serve the web UI on 127.0.0.1:7749
evilcode serve -status                # inspect the daemon
evilcode serve -stop                  # stop it cleanly
evilcode attach [session]             # attach to an existing daemon session
evilcode attach -l                    # list sessions
evilcode resume --from claude <id-or-path>

evilcode run hands the prompt to the daemon and exits. The agent, tools, background tasks, and session can keep working after that shell closes. Use --wait when you need the answer streamed back, or --local when you want the old in-process one-shot mode.

evilcode attach needs a daemon that is already running. A TUI window can close without ending the session; another window can reconnect to it later. A session with no window is kept hydrated for ten minutes after its last turn/window activity, then it is cleanly closed and unloaded. Its transcript remains available for resume.

The web UI

evilcode serve -web starts an opt-in HTTP surface beside the unix socket, bound to 127.0.0.1:7749 by default ([webui] addr / serve -web-addr to change). [webui] enabled = true starts the same listener automatically when ec starts the daemon; it is disabled by default. Token auth is enabled by default; set [webui] require_auth = false only when the network boundary (for example, Tailscale ACLs) is the complete trust boundary. With auth enabled, the first start mints a token at <socket>.web-token (mode 0600) and prints a one-time tokenized URL — open it in a browser and the token is exchanged for a cookie; later requests are cookie- or Bearer-authenticated. Mutating requests must come from the same origin in either mode. When a trusted HTTPS proxy supplies X-Forwarded-Host, opt into that behavior with [webui] trust_forwarded_headers = true; leave it false for any listener reachable by direct clients, because forwarded headers are otherwise forgeable. The UI covers the full chat + roster surface: live transcripts over SSE, deep history from the durable session store, model/effort switching, slash commands, asks, spawn, and worker pokes. On a phone it is a roster-first webapp: Add to Home Screen (Share → "Add to Home Screen") installs it as a standalone app with the daemon's own theme; while a turn is running the screen is kept awake when the browser allows it (silent no-op otherwise — plain HTTP off the loopback host is not a secure context). A browser sitting on the roster holds no stream, so a phone-first setup should run evilcode serve -idle 0 to keep the daemon alive. The blessed remote path is Tailscale to the loopback bind, not a LAN bind.

From inside evilcode: /tailscale

/tailscale in the TUI does the whole setup in one step: it starts the web listener if it is off (trusting forwarded headers for this daemon run, so the proxied origin works), runs tailscale serve --bg for it, and draws a QR code of the https://<node>.<tailnet>.ts.net URL — token included when auth is on. Point a phone camera at it and the web UI opens. Undo the proxy with tailscale serve reset (only if the node has no other Serve routes); the manual equivalent is below.

Test from another Tailscale device

Keep evilcode on its loopback bind and let Tailscale provide the tailnet-only HTTPS proxy. On the daemon machine: Set [webui] trust_forwarded_headers = true only for this proxy-only setup; otherwise leave it false so direct clients cannot forge the forwarded origin.

evilcode serve -status                 # check whether a daemon is already running
evilcode serve -web -idle 0             # start with web access and no idle exit
tailscale serve --bg 7749               # proxy HTTPS on this node to 127.0.0.1:7749
tailscale serve status                  # copy the https://<node>.<tailnet>.ts.net URL

If a daemon is already running without web access, stop it only between turns and restart it with the second command:

evilcode serve -stop
evilcode serve -web -idle 0

If [webui] require_auth = false is set, open https://<node>.<tailnet>.ts.net/ directly on the other device. No evilcode token or cookie is required; access is controlled entirely by Tailscale membership and ACLs.

With auth enabled, evilcode prints a one-time tokenized URL on the first web start. Replace its http://127.0.0.1:7749 origin with the HTTPS URL from tailscale serve status, then open https://<node>.<tailnet>.ts.net/?token=<token> on the other device. The redirect removes token from the address bar and stores an HttpOnly cookie. If the token was minted previously, read the 64-character value from <socket>.web-token on the daemon machine; the default is $XDG_RUNTIME_DIR/evilcode.sock.web-token when XDG_RUNTIME_DIR is set. Treat the token like a password and do not paste it into shell history, issues, or logs.

The other device must be signed in to the same tailnet and allowed by its ACLs. From the page, exercise a new session, a prompt, an ask, an interrupt, and a reload/reconnect. Use evilcode serve -status on the daemon machine to watch the session and client counts. Do not use tailscale funnel and do not bind evilcode to a LAN or Tailscale IP for this test.

When finished, remove the proxy (use this only if the node has no other Serve routes) and stop the test daemon:

tailscale serve reset
evilcode serve -stop

Requirements

Linux only.

At runtime, rg powers the grep tool and tmux is used by the probe rig. These are optional when you do not use the features that need them:

  • gopls for LSP features
  • mmdc for Mermaid diagrams
  • img2sixel for terminals without Kitty graphics support

Models and configuration

The config file is ~/.config/evilcode/config.toml, unless EVILCODE_CONFIG points somewhere else. A missing config is okay; evilcode starts with sensible defaults. An invalid one fails at startup with a single error listing every problem's TOML path, so a misconfigured file can be fixed in one edit instead of one restart per field.

Ollama Cloud is the easiest route to try. With OLLAMA_API_KEY, the default model is deepseek-v4-flash:0731@ollama-cloud (reasoning effort high); without a key, the local Ollama route is used when it is available. With OPENCODE_API_KEY, the default routes to OpenCode Go — opencode's subscription gateway for open coding models (glm-5.3-flash@opencode-go) — whose picker entries carry context windows, vision, and per-model reasoning levels. You can also use OpenAI-compatible providers, DeepSeek, Codex, Ollama Local, or the deterministic mock provider used by tests.

For example:

default_model = "deepseek-v4-flash:0731@ollama-cloud"

[[provider]]
name = "ollama-cloud"
kind = "ollama"
base_url = "https://ollama.com"
api_key_env = "OLLAMA_API_KEY"

[[model]]
name = "glm-5.2:cloud"
context_window = 262144

[display]
theme = "catppuccin-frappe"  # catppuccin-frappe | dracula | nosferatu | gloom | daywalker
inline_diffs = true
centered = false

[features]
memory = true
advisor = false
max_steps = 0            # 0 means unlimited tool rounds

If the Codex CLI is installed and logged in, evilcode can discover its OAuth account from the normal Codex auth file. /model and -m select a model; /reasoning changes the advertised reasoning effort when a provider supports it. The model catalog is fetched live from each provider once per session and then cached, so newly released models will not appear until you run /refresh-model-list (or restart) — it drops the cache and re-runs discovery against every configured provider.

API keys are entered with /login <provider> (or /login to pick from a selector), which saves them to the user-only config. OpenCode Go's key comes from opencode.ai/auth — sign in, subscribe to Go, and paste the key at /login opencode-go. Its bundled model metadata is generated from models.dev; if the gateway publishes a model the metadata lacks, it still appears (without window or reasoning details) via the live listing, and go run ./cmd/gen-opencode-models regenerates the table.

One gateway quirk is handled transparently: the muse-spark, grok, and gpt-5.6 families are not served over chat completions (they return 500 there) and are routed to the gateway's /responses endpoint instead, matching how opencode's own client addresses them.

Use /connect brave to enable the optional Brave-backed web_search tool. Credentials are masked and stored in the user-only config, or can be supplied through BRAVE_SEARCH_API_KEY / BRAVE_API_KEY.

Ollama Cloud exposes no usage API, so the Cloud Usage widget reads https://ollama.com/settings with your browser session. Paste the __Secure-session cookie value (DevTools → Network → any ollama.com request → Cookies) with /connect ollama-usage — it is stored masked in the user-only config, like /connect brave stores its key — or set OLLAMA_SESSION_COOKIE (env beats the saved value; a value with a ; or a cookie-name prefix such as __Secure-session= is treated as a full Cookie: header, anything else as the bare __Secure-session value, so base64 = padding inside the token is fine). /connect ollama-usage status reports presence without printing it. The widget then shows Session and Weekly quota bars, each slice colored per model. Treat the cookie like a password — it is a live session credential. The scrape is unofficial; if the page changes, the widget reports that instead of guessing.

Repository-specific defaults can live in .evilcode.toml at the repository root. Model and role overrides are supported there; credentials are deliberately not.

The useful bits

Terminal UI

The TUI is built on the Charm stack and is designed for a real terminal rather than a browser imitation. It includes:

  • streaming Markdown and syntax-highlighted code
  • clickable shell code blocks that copy clean commands to the clipboard
  • inline diffs, a pinned diff view, and whole-file change gutters
  • a split view that follows the agent's file activity: Ctrl+L opens a live pane on each touched file scrolled to the change, Ctrl+Q closes it, and the wheel scrolls whichever side the mouse is over. While a call streams, the transcript row types its command or path in place, and the live pane becomes a terminal for bash (the streamed output appends under the command, autoscrolling unless you scrolled up) or an assembling view for write/edit/multiedit against the on-disk file; any other tool shows a dimmed raw-args preview. The full tool payload still arrives at start and result, so a reconnect or a second window rebuilds the call exactly.
  • model and reasoning pickers
  • /help, /theme, /diff, /compact, /rewind, and history search with Ctrl+R
  • Kitty and sixel image display when the terminal supports it

The start page shows existing sessions and a live preview so resuming one does not feel like guessing which creature name you meant.

Tools

The built-in tools include read, write, edit, glob, grep, bash, bg, ask, todo, git helpers, optional web search, LSP, and MCP servers.

Long shell commands can move into the daemon's background task manager. Use bg status, bg output, bg wait, or bg cancel to manage them. A command copied from a bash or fish code block is cleaned of comments and trailing whitespace before it reaches the clipboard.

grep can include the surrounding function, method, or type. edit supports hash- anchored changes so stale context is refused instead of being applied fuzzily.

Sessions and memory

Every message is written as it arrives. /compact, /rewind, /shake, and /handoff rewrite logs atomically, and resumed sessions restore their working directory and last model. The daemon marks idle shutdowns cleanly; if it is stopped while a turn is genuinely active, that run is left crash-detectable so it cannot be mistaken for a completed answer.

Context management

Compaction is a port of oh-my-pi's engine (docs/plan-compaction-port.md). It fires when the context is genuinely near full — the window minus max(15%, 16384 tokens) — plus recovery paths when a provider rejects the request (overflow compacts everything; a length cap keeps the tail). There is no speculative trigger: no growth projection, no topic-shift guessing.

The summary comes from a candidate chain — the session model first, then every configured role model, then the largest-context model available — with per-model retry and backoff. Its format is fixed (Goal / Progress / Decisions / Next Steps), validated mechanically: a summary that dropped the user's goal or answered the transcript's trailing question is rejected and retried once before falling to the next model. Exact file paths are injected from the actual tool calls, not trusted to the model.

Between turns, superseded reads of the same file are blanked in the log, so context grows slowly and compaction stays rare. /shake is a no-LLM alternative: it elides big tool results and fenced blocks, keeping the originals in a recoverable document. /handoff continues in a fresh session from a generated document; the original stays on disk untouched.

[compaction]
strategy = "context-full"   # context-full | handoff | shake | off
reserve_tokens = 16384
keep_recent_tokens = 20000
auto_continue = true
prune_reads = true

Memory is optional and best-effort. Relevant facts can be recalled into a turn, and a summary is written when a session is actually torn down. If the embedding provider is unavailable, lexical matching remains available.

Unattended work and swarms

/overnight works through a todo list without a window attached. It is bounded by turns, tokens, wall clock, and stalled progress, and writes a small report when it stops.

Sessions can delegate work to headless workers through spawn_worker or /summon. spawn_worker runs in the foreground: the turn waits, and the worker's result — validated against the JSON Schema the parent supplies — comes back directly as the tool result, so delegation reads like a blocking call. /summon starts a worker without waiting; its result arrives as a message when it finishes. Heartbeats make a silent worker visible as stale instead of leaving everyone waiting forever. Shared file and todo state lets a small swarm coordinate without each worker keeping a private copy.

Keys

Key Action
Enter send; queue while a turn is running
Shift+Enter, Alt+Enter, trailing \ newline
Esc cancel overlays, interrupt, then clear input
Ctrl+C detach; press twice to quit, stopping the daemon when nothing else is live
Ctrl+R search prompt history
Ctrl+G toggle a scroll bookmark
Alt+B send a running tool to the background
Ctrl+L toggle the live split view
Ctrl+Q close the split
PgUp, PgDn scroll one page
, on empty input scroll one line

Run /terminal-setup if your terminal does not distinguish Shift+Enter from Enter.

Data and privacy

evilcode does not send telemetry and does not require an evilcode account. Local state lives under ~/.local/share/evilcode/:

  • sessions/*.jsonl — conversations and tool history
  • prompt-history.jsonl — local prompt history
  • memory.jsonl — optional durable memory
  • session blobs and detached overnight reports

Treat those files like private work logs. They can contain prompts, code, command output, and anything a model was shown.

Contributing

This project began as a tool for one person, so the edges are opinionated and some seams are tighter than they should be. That is exactly why outside feedback is useful.

Feature requests, bug reports, documentation fixes, and pull requests are welcome. If you are changing behavior, please include a focused test where practical. Before opening a PR, run:

go test ./...
go test -race ./...
go vet ./...

The probe rig can exercise a real binary in a tmux pane and capture PNG frames:

go build -o evilcode ./cmd/evilcode
go test -tags probe ./probe/...
probe/probe.sh boot
probe/probe.sh keys "/help" Enter
probe/probe.sh png help
probe/probe.sh kill

Project layout

cmd/evilcode        executable entrypoint and updater
internal/agent       model loop, events, and hooks
internal/tui         terminal UI and attach mirror
internal/attachcmd   socket client and remote TUI wiring
internal/tuicmd      default TUI entrypoint
internal/runcmd      local and daemon-backed headless runs
internal/servecmd    daemon entrypoint
internal/provider    Ollama, OpenAI-compatible, DeepSeek, OpenCode Go, Codex, and mock providers
internal/tools       built-in tools
internal/daemon      server, sessions, reconnects, and swarms
internal/wiring      shared provider/tool/session construction
internal/memory      semantic memory bank

evilcode is GPL-3.0. If you try it, find something rough, or have an idea that would make it more useful, let me know.