hey
A minimal terminal AI coding agent in Rust. A Swiss-army knife for the terminal: the core is just an agent loop plus three traits (Provider / Tool / Output). Everything else — skills, MCP tools, sessions, context pruning — is plain files and protocols, no language-level plugins.
Features
- Protocol-agnostic core: a unified IR (
src/llm/ir.rs) plus adapters — OpenAI-compatible SSE (default), Anthropic (M2), Google Gemini (M5); add a protocol as a config-declared adapter without touching the kernel. - Multi-provider:
[provider.*]sections, CLI/env/config/default resolution,provider/modelprefix on--model, no-auth requests whenapi_keyis omitted (Ollama/vLLM-friendly). - Debugging:
--effort <off|minimal|low|medium|high|xhigh|max>— unified reasoning depth (pi / Claude Code compatible): OpenAI sendsreasoning_effort(off→none), Anthropic uses adaptive thinking +output_config.effort(legacy endpoints:budget_tokens), Google mapsthinkingLevel(gemini-3) orthinkingBudget(2.5)./effort+/thinkingREPL commands switch it at runtime. HTTP/HTTPS/SOCKS5 proxy withNO_PROXYsupport. Verified with localgostSOCKS5 gateway. - Thinking round-trip: reasoning (
reasoning_contenton DeepSeek-style endpoints, Claude thinking blocks, Gemini thought parts) is accumulated and passed back verbatim on the next turn — required by DeepSeek thinking mode (400 otherwise). - 5 built-in tools (
bash/read_file/write_file/edit/grep): bash output is heuristically filtered to save tokens (--rawescapes), butgit status/diff/log/showoutput is never filtered — git context always arrives complete. Plus acompresstool for context summarization. - MCP clients (
[mcp.*]stdio / HTTP servers): tools are imported into the registry as<server>_<tool>(dots sanitized to_per OpenAI tool-name rules), startup failures only warn. Real-world verified: Context7 remote MCP (resolve-library-id→query-docs), and@modelcontextprotocol/server-filesystem(read/write/list/edit). - Skills (
.hey/skills/+~/.config/hey/skills/): frontmatter-validated markdown instructions, progressive disclosure in the system prompt, and a built-in guide so the agent can teach itself new skills. - Sessions: append-only JSONL under
~/.local/state/hey/sessions/, resume with-c/--resume <id>; oversized resumes are rebuilt as a lossless summary plus a recent window, and the summary is persisted to the session file so repeated resumes reuse it without re-running the LLM. All requests share connect timeouts; reasoning models stay safe by default. - Token frugality: layered retry (request + turn), DCP-style context pruning (dedup / error cleanup / compress / nudge), dynamic system prompt with AGENTS.md injection (warns at >50K chars, no truncation).
- Trust:
.hey/resources are only loaded after an interactive confirmation (persisted to~/.config/hey/trust.json);--approveopts in explicitly.
Install
# or: cargo install hey --git https://github.com/<you>/hey
Zero system dependencies (rustls); works on Linux/macOS (bash tool needs a POSIX shell). On Windows: the bash tool auto-detects sh in PATH (Git Bash / WSL) and falls back to PowerShell; you can pin a shell with [tools] shell.
Configuration
A complete annotated example covering every section (providers / MCP / proxy / agent / retry / compaction / permission / tools) lives in docs/config.example.toml.
--config > .hey/config.toml > ~/.config/hey/config.toml; env overrides: HEY_PROVIDER / HEY_MODEL (selection only), HTTP(S)_PROXY / ALL_PROXY / NO_PROXY for the proxy, and RUST_LOG for log verbosity (default warn; e.g. RUST_LOG=info or RUST_LOG=hey::mcp=debug to debug MCP). ${VAR} references in config values are expanded at load. Model selection is standard: -m provider/model (like codex/opencode), or zero-config via --base-url. Example:
[]
= "openai"
= "gpt-4o"
[]
= "https://api.openai.com/v1"
= "${OPENAI_API_KEY}" # omit for no-auth (Ollama/vLLM/gateway)
= "high" # default reasoning depth for this provider
[]
= "anthropic"
= "https://api.anthropic.com/v1"
= "${ANTHROPIC_API_KEY}"
= ["claude-sonnet-4-5"]
= "high" # adaptive thinking + output_config.effort (new API)
# max_tokens = 16000 # optional; default = auto-sized for the effort level
# legacy_thinking = true # old API/compat gateways: maps effort to budget_tokens
[]
= "google"
= "https://generativelanguage.googleapis.com/v1beta"
= "${GEMINI_API_KEY}"
= ["gemini-2.5-flash"]
= "high" # maps to thinkingBudget (Google has no effort concept)
[] # no auth
= "http://localhost:11434/v1"
= ["qwen3:14b"]
[]
= "http://127.0.0.1:7890" # default: env HTTP(S)_PROXY / ALL_PROXY
= 30 # connect timeout (safe default; no normal connection exceeds this)
= 0 # stream read timeout; 0 = disabled (default — reasoning models can be silent)
[]
= 20
= 40000
= true # run same-turn tool calls in parallel (default true; false = serial)
[]
= true
= 5
= 2000
= 60000
= true
= true
[]
= true
= 4
= 0.7
= true
[]
= ["bash: cargo *"] # default: allow all (pi-style); set rules to restrict; unmatched → blocked
[]
# 可选:外部过滤命令(bash 工具输出过 stdin 给命令,取 stdout 为过滤结果;
# 失败/为空回退内置启发式;raw=true 时绕过)。缺省 = 内置过滤。
# bash_filter = "grep -E 'error|warning|FAILED'"
# shell = "powershell" # Windows 下默认找 sh(Git Bash/WSL)否则回退 PowerShell;此配置可显式钉住
# timeout_secs = 120 # bash 默认超时秒数;0 = 禁用(无限等待);模型参数 timeout_secs 可覆盖
# Remote MCP (HTTP/SSE): no local subprocess, direct HTTP connection
# Real-world verified with Context7 (resolve-library-id → query-docs):
# [mcp.context7]
# url = "https://mcp.context7.com/mcp"
# respawn = false
# MCP filesystem (stdio): 12 tools injected (read_text_file / write_file / list_directory / search_files / etc.)
[]
= "npx"
= ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allow"]
# respawn = true # crash the subprocess restarts automatically (default false:
# # transport failure is reported as-is; recover with REPL /reload)
Usage
hey # REPL (cross-turn session memory)
hey "explain this repo" # one-shot
hey -p "summarize README.md" < notes.txt # print mode + stdin pipe
hey --output json "run the tests" # JSONL event stream for scripts/CI
hey -m zen/deepseek-v4-flash "..." # standard provider/model selection
hey -m kilo/tencent/hy3:free "async Rust" # kilo gateway (free hy3 reasoning model, OpenRouter-compatible)
hey --base-url http://localhost:11434/v1 -m qwen3:14b "..." # zero-config ad-hoc provider
hey --base-url https://api.x.com/v1 -m gpt-4o --api-key "$KEY" "..." # ad-hoc + protocol openai
hey -t high "..." # reasoning depth (--effort: off|minimal|low|medium|high|xhigh|max)
hey @src/main.rs "review this file" # @file expansion
hey -c # continue latest session
hey --resume <id> "..." # resume a specific session
hey --sessions-dir /tmp/sess -p "..." # session store directory (default ~/.local/state/hey/sessions)
hey --approve # trust this project's .hey/ resources (headless)
hey --no-mcp / --no-skills # disable extensions
--output json emits one JSON object per line (events from the agent loop):
type |
fields | emitted |
|---|---|---|
text_delta |
delta |
streamed assistant text |
thinking_delta |
delta |
streamed reasoning (if any) |
tool_call |
name, args |
tool invocation (args is a JSON string) |
tool_result |
name, ok, output |
tool result |
usage |
input, output |
token counts (per turn) |
finished |
reason, turns |
turn completion |
error |
message |
fatal error (exit code 1) |
Every event is flushed immediately (safe for pipes; survives Ctrl-C).
REPL commands: /help, /model [provider]/name (switch live), /effort <level> (switch reasoning depth live), /thinking (toggle thinking display), /usage, /compact, /reload, /resume <id>|- (switch session), /sessions, /sessions rm <id>, /sessions prune <keep>, /skill:<name>. Ctrl-C cancels the current turn; Ctrl-C again exits.
Startup output
On REPL launch, hey prints a one-line summary of what it loaded (provider, skills, MCP servers):
hey 0.1.0
provider: kilo/tencent/hy3:free
skills: 2 (review-pr, fix-issue)
mcp: 2/2 connected: context7, filesystem (16 tools)
This is on by default (pi-style: you see what's loaded). To suppress it, set
quiet_startup = true in your config, or pass --quiet on the command line.
--verbose forces the summary even when quiet_startup = true. Errors (e.g.
[hey] mcp 'context7' skipped: ...) are always shown regardless of this setting.
Example config for local (no-auth) use
[]
= "http://localhost:11434/v1"
= ["qwen3:14b"]
hey -m local/qwen3:14b "hello" # or: --base-url http://localhost:11434/v1 -m qwen3:14b
Example: kilo gateway (hy3 reasoning model)
kilo provides free reasoning models via an OpenAI-compatible endpoint.
The API key is managed in ~/.bashrc (export KILO_API_KEY='...'), not hardcoded in the config:
[]
= "openai"
= "https://api.kilo.ai/api/openrouter"
= "${KILO_API_KEY}"
= ["tencent/hy3:free"]
= "low"
= 262144
hey -m kilo/tencent/hy3:free "explain async Rust"
Example: SOCKS5 proxy + filesystem MCP (real-world combo)
Proxies the LLM traffic through a local SOCKS5 tunnel while keeping a local stdio MCP server (filesystem) running normally — MCP traffic stays on the loopback, only LLM traffic goes through the proxy. Verified with the script in scripts/smoke-real.sh.
[]
= "socks5://127.0.0.1:1080" # any SOCKS5/HTTP proxy; also picked up from ALL_PROXY env
[]
= "npx"
= ["-y", "@modelcontextprotocol/server-filesystem", "."]
= true
hey -m kilo/tencent/hy3:free "@src/main.rs review the Ctrl-C cleanup"
# LLM traffic → SOCKS5 → upstream API
# MCP filesystem tools → direct loopback (unaffected by proxy)
Extending hey
- Skills: drop a
SKILL.mdwithname+descriptionfrontmatter into.hey/skills/<name>/(or~/.config/hey/skills/<name>/). The agent itself can create new skills withwrite_file, then/reloadto activate them in the next turn — file-level self-evolution. - MCP: any stdio or HTTP MCP server becomes tools automatically. Name conflicts: later registration wins (MCP overrides built-ins).
Architecture
src/
├── llm/ IR + Provider trait + adapters (openai / anthropic / google / retry)
├── config.rs TOML config + env expansion + provider selection + [mcp.*]
├── proxy.rs proxy resolution (CLI > config > env, socks5, NO_PROXY)
├── agent.rs agent loop: request → stream → tools → backfill (re-try / nudge /
│ session persist / DCP dedup), plus:
│ ├── agent/prompt.rs system prompt (AGENTS.md + skills injection)
│ ├── agent/context.rs token estimate / pair compaction / thinking prune
│ ├── agent/execution.rs ToolRunner: precheck → parallel/serial run → ordered backfill
│ └── agent/compress.rs compress tool (lossless summary; resume rebuild reuses it)
├── tools.rs Tool trait + bash output filter + 5 built-in tools
├── skills.rs frontmatter validation + project/global scan + progressive disclosure
├── sessions.rs JSONL sessions (~/.local/state/hey/sessions/)
├── mcp.rs MCP stdio client (JSON-RPC 2.0)
├── trust.rs project trust gate (~/.config/hey/trust.json)
├── ui.rs terminal / JSON / test output
├── prompt.rs @file expansion + CLI prompt assembly (stdin pipe + args)
├── completer.rs REPL tab completion (/help /quit /model /etc)
├── ctrl_c.rs Ctrl-C handler + termios save/restore
├── lib.rs library entry (kernel API for embedders)
└── main.rs CLI + REPL (/usage /compact /model /reload /skill:<name>)
Design: docs/DESIGN.md (Chinese). Comparison with pi/other agents: docs/COMPARISON.md, docs/VS_PI.md.
Testing
Unit tests (pure functions: config/retry/filter/compaction/skills/sessions/trust) + integration tests (self-written MockServer, FakeProvider, FlakyProvider, fake MCP server) — no real network involved.
Development
Three quality gates are enforced locally and in CI (cargo test / cargo clippy --all-targets -D warnings / cargo fmt -- --check); a commit must pass all three plus a release build. MSRV: 1.88 (edition 2024 + let-chains). CI (GitHub Actions) additionally runs a RustSec dependency audit. Formatting follows the default cargo fmt style — no custom rustfmt.toml. Optional: scripts/smoke-real.sh runs all three protocol adapters against a real multi-protocol gateway (key read from ~/.pi/agent/auth.json, not committed; requires a cargo build --release first).
Benchmark
Real-gateway coding benchmark (systechn-pro / Qwen3.6-35B, on this repo, judged by cargo test + cargo clippy in an isolated git worktree):
| task | result | evidence |
|---|---|---|
| REPL history persistence | solved | 161 tests green, incl. new roundtrip + 0600-permission test |
| compact_messages boundary tests | solved | 162 tests green, 2 new tests (also corrected the spec's keep_tail assumption) |
| session-append error escalation | solved (3rd try) | 161 tests green; first error!/then warn! via Mutex<Option<HashSet>>, new test, no new deps — unlocked by the tooling fixes below |
| .hey/task.md checkpoint guidance | solved | finished: complete, new system-prompt test |
| parse_args debug logging | solved (2nd try) | 163 tests green; added debug! logs (tool+args_len, no secrets) + 3 tests, fixed the null-handling test to match existing behavior |
solved 5/5. The two initially-failed tasks were unlocked by real-world tooling fixes (see commit log): edit failure now reports the old_text head + a re-read instruction, and the system prompt gained edit discipline (no blind retries, small steps, git diff self-check). The final T3 run implemented + self-verified (release tests green) in one pass instead of an edit/fix loop. Run with scripts/bench-real.sh (needs the local systechn gateway + cargo build --release); per-task artifacts land in target/bench/ (git-ignored).