Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

meka is a general-purpose AI agent harness that provides LLMs with a rich set of tools — web search, shell execution, file editing, and more — to accomplish complex tasks. Use it as a natural-language shell, a system diagnostic helper, a research or data-analysis assistant, for general Q&A, or to add agentic capabilities to other applications.

meka [r] > find all Rust files in this project and count the lines of code

Instead of remembering find . -name '*.rs' | xargs wc -l, you describe what you want and the agent figures out how to do it.

Features

  • Natural language interface – describe what you want instead of memorizing syntax
  • Built-in tools – file read/write/edit, glob search, regex content search (ripgrep), web fetch, web search, shell command execution
  • Scratchpad – session-scoped working memory for the agent to store and retrieve intermediate results
  • Sub-agents – delegate research tasks to sub-agents that inherit the parent’s permission level
  • Multiple LLM providers – OpenAI API, OpenAI Codex (ChatGPT subscription), Claude API, and Claude OAuth (Claude subscription), with support for any OpenAI-compatible endpoint
  • MCP support – extend the agent with tools from external MCP servers
  • Permission system – control what the agent can do (none/read/ask/write), switchable mid-session
  • Session management – conversations are persisted in SQLite; resume, export, or compact any session
  • Streaming output – responses stream to the terminal in real time with syntax highlighting
  • Interactive and one-shot modes – use it as a REPL or pipe a single prompt
  • Extended thinkingclaude-api and claude-oauth support extended thinking for complex reasoning

How It Works

  1. You type a natural language instruction
  2. meka sends it to the configured LLM along with tool definitions and a system prompt
  3. The LLM decides which tools to call (if any) and returns text and/or tool calls
  4. meka executes the tool calls, feeds results back to the LLM, and repeats until the LLM is done
  5. The final response is rendered as Markdown in the terminal

Installation

meka is written in Rust and builds as a single binary.

Pre-Built Binaries

Download the latest release for your platform from the GitHub Releases page.

PlatformArchive
Linux (x86_64)meka-linux-amd64.tar.gz
macOS (Apple Silicon)meka-macos-arm64.tar.gz
Windows (x86_64)meka-windows-amd64.zip

Extract the binary and place it somewhere on your $PATH:

# Linux/macOS
tar -xzf meka-*.tar.gz
cp meka ~/.local/bin/

Cargo Install

If you have Rust installed, you can install meka directly from the Git repository:

cargo install --locked --git https://github.com/k4yt3x/meka.git

This builds the latest version from source and installs it to ~/.cargo/bin/.

Building from Source

Prerequisites

  • Rust (edition 2024, requires Rust 1.85+)
  • A C compiler (for the bundled SQLite)

Build

git clone https://github.com/k4yt3x/meka.git
cd meka
cargo build --release

The binary will be at target/release/meka. Copy it somewhere on your $PATH:

cp target/release/meka ~/.local/bin/

Verify

meka --version
meka --help

Quick Start

1. Run the Setup Wizard

On first launch, meka automatically starts an interactive setup wizard:

meka

The wizard will guide you through:

  1. Provider selection — Choose between claude-oauth, claude-api, or openai-api
  2. Authentication — OAuth login (claude-oauth) or API key entry (claude-api, openai-api)
  3. Model selection — Enter the model name to use
  4. Base URL — Optionally set a custom API endpoint

The wizard writes your configuration to ~/.config/meka/config.toml. You can re-run it at any time with meka setup.

You can also create the config file manually or use environment variables (OPENAI_API_KEY, MEKA_PROVIDER, etc.) and CLI flags (--provider, -m) as overrides. See Configuration for all options.

2. Start Using meka

After setup, you will see a prompt:

meka [r] >

You will see a prompt:

meka [r] >

The [r] indicates read permission mode (the default). The agent can read files and search, but cannot write files or run commands.

3. Ask It Something

meka [r] > what files are in the current directory?

The agent will use the find_files tool to list files and describe them.

4. Enable Write Mode

Press Shift+Tab to cycle the permission to write mode:

meka [w] >

Now the agent can execute commands and modify files:

meka [w] > create a file called hello.txt with the text "hello world"

5. One-Shot Mode

For quick tasks without entering the interactive shell:

meka "what is my current working directory?"

The process exits after the agent responds.

6. Continue a Previous Session

To pick up where you left off, continue the last session:

meka -c

Or resume a specific session by its UUID:

meka -c 550e8400-e29b-41d4-a716-446655440000

See Sessions for more details.

Configuration Overview

The recommended way to configure meka is with a config file at ~/.config/meka/config.toml:

[provider]
name = "openai-api"
model = "gpt-4o"
api_key = "sk-..."

This is all you need to get started. See Config File for the full reference.

Required Settings

meka requires three settings to function. If any are missing, it prints an error with setup instructions:

SettingConfig KeyEnv VarCLI Flag
Providerprovider.nameMEKA_PROVIDER--provider
Modelprovider.modelMEKA_MODEL-m, --model
API Keyprovider.api_keyOPENAI_API_KEY or CLAUDE_API_KEY

Override Layers

Configuration is layered. Higher-priority layers override lower ones:

  1. CLI flags – per-invocation overrides (--provider, --model, --base-url, -p)
  2. Environment variables – useful for CI, containers, or temporary overrides (MEKA_PROVIDER, etc.)
  3. Config file – persistent settings in ~/.config/meka/config.toml
  4. Built-in defaults – permission defaults to read, streaming defaults to on

For example, --model gpt-4o-mini on the command line overrides both MEKA_MODEL and provider.model in the config file.

API Key Resolution

The API key environment variable depends on the configured provider:

  • Provider openai-api: reads OPENAI_API_KEY
  • Provider openai-codex: reads OPENAI_CODEX_TOKEN, otherwise loads the OAuth token saved by meka setup
  • Provider claude-api: reads CLAUDE_API_KEY
  • Provider claude-oauth: reads CLAUDE_OAUTH_TOKEN, otherwise loads the OAuth token saved by meka setup

If the environment variable is not set, it falls back to provider.api_key (or provider.oauth_token for OAuth providers) in the config file.

Config File

meka looks for a TOML configuration file at a platform-specific location:

PlatformPath
Linux~/.config/meka/config.toml ($XDG_CONFIG_HOME/meka/config.toml)
macOS~/Library/Application Support/meka/config.toml
Windows%APPDATA%\meka\config.toml

The config file is optional. If it does not exist, meka silently skips it.

Set the MEKA_CONFIG_DIR environment variable to override the default location entirely — the value points at the meka directory itself (contains config.toml and skills/). Useful for tests, portable installs, and isolating a per-project config from your global one.

Format

[provider]
name = "openai-api"
model = "gpt-4o"
api_key = "sk-..."
base_url = "https://api.openai.com/v1"

All fields under [provider] are optional individually – you can set some in the config file and override others with environment variables or CLI flags.

Fields

provider.name

The LLM provider to use.

ValueDescription
openai-apiOpenAI Chat Completions API (also works with OpenAI-compatible APIs)
openai-codexOpenAI Responses API via ChatGPT subscription OAuth, against chatgpt.com/backend-api/codex
claude-apiClaude Messages API with x-api-key auth
claude-oauthClaude Messages API via Claude Code OAuth (fingerprinting + attestation)

provider.model

The model identifier to send to the provider. Examples:

  • gpt-4o, gpt-4o-mini, gpt-5 (OpenAI)
  • claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5 (Claude)
  • Any model supported by an OpenAI-compatible endpoint

provider.api_key

The API key for authentication. It is recommended to use environment variables (OPENAI_API_KEY or CLAUDE_API_KEY) instead of storing the key in the config file.

provider.oauth_token

OAuth access token for the claude-oauth and openai-codex providers. Equivalent env vars: CLAUDE_OAUTH_TOKEN (claude-oauth) or OPENAI_CODEX_TOKEN (openai-codex). Run meka setup to obtain one interactively. The token is saved to the database on first use and loaded automatically on subsequent launches.

provider.oauth_token_url

Custom OAuth token refresh endpoint. Defaults:

  • https://api.anthropic.com/v1/oauth/token for claude-oauth
  • https://auth.openai.com/oauth/token for openai-codex

provider.base_url

Custom API base URL. Useful for:

  • Self-hosted models via Ollama (http://localhost:11434/v1)
  • OpenRouter (https://openrouter.ai/api/v1)
  • Other OpenAI-compatible API providers

If not set, defaults to:

  • https://api.openai.com/v1 for the openai-api provider
  • https://chatgpt.com for the openai-codex provider (request path is /backend-api/codex/responses)
  • https://api.anthropic.com for the claude-api and claude-oauth providers

provider.reasoning_effort

Reasoning effort level for OpenAI o-series models. When set, the reasoning_effort parameter is included in API requests and max_completion_tokens is used instead of max_tokens.

Accepted values: low, medium, high. Omitted by default.

[provider]
reasoning_effort = "medium"

provider.effort

claude-oauth only. Controls the output_config.effort field that the effort-2025-11-24 beta unlocks for adaptive-thinking-capable models (opus-4-6, sonnet-4-6). Higher values give the model more time to think; the field is ignored on non-effort-capable models.

Accepted values: low, medium, high. Defaults to high. Unrecognised values fall back to high and are logged at warn.

[provider]
effort = "medium"

provider.redact_thinking

claude-oauth only. When true, meka sends the redact-thinking-2026-02-12 beta header so the API returns redacted_thinking blocks instead of full thinking summaries — useful when you don’t render thinking in the UI and want the smaller response. Defaults to false (full thinking summaries).

Caveat: redacted_thinking blocks carry a signed payload that must be replayed verbatim on subsequent turns; meka currently flattens them to [redacted] text on receipt, which means multi-turn conversations after enabling this flag may be rejected by the server. Treat as experimental.

[provider]
redact_thinking = true

provider.device_id

claude-oauth only. Stable per-device identifier embedded in metadata.user_id to mirror Claude Code’s ~/.claude.json device ID (getOrCreateUserID in utils/config.ts).

If unset, meka first tries to adopt userID from ~/.claude.json (so meka and Claude Code on the same machine look like the same device). If that file is missing or has no userID, meka generates a 64-character hex string. Either way, the resolved value is persisted back to this same config file under [provider].device_id. This file write only happens for the claude-oauth provider — other providers don’t need a device ID.

You can supply your own value if you want to control attribution explicitly:

[provider]
device_id = "your-stable-id-here"

Examples

OpenAI API

[provider]
name = "openai-api"
model = "gpt-4o"
# API key via env: export OPENAI_API_KEY=sk-...

Claude API

[provider]
name = "claude-api"
model = "claude-opus-4-6"
# API key via env: export CLAUDE_API_KEY=sk-ant-api03-...

Claude OAuth

[provider]
name = "claude-oauth"
model = "claude-opus-4-6"
# Run `meka setup` to perform the OAuth login, or:
# export CLAUDE_OAUTH_TOKEN=sk-ant-oat01-...

OpenAI Codex (ChatGPT subscription)

[provider]
name = "openai-codex"
model = "gpt-5"
# Run `meka setup` to perform the OAuth login, or:
# export OPENAI_CODEX_TOKEN=...

Ollama (local)

[provider]
name = "openai-api"
model = "llama3"
api_key = "unused"
base_url = "http://localhost:11434/v1"

OpenRouter

[provider]
name = "openai-api"
model = "anthropic/claude-sonnet-4.6"
base_url = "https://openrouter.ai/api/v1"
# API key via env: export OPENAI_API_KEY=sk-or-...

[display]

Settings for output formatting.

display.render_mode

Output render mode. Equivalent to the --render-mode CLI flag.

ValueDescription
batSyntax-highlighted markdown via bat (default)
termimadTerminal formatting via termimad (box-drawn code blocks, reflowed paragraphs). Alias: rich
rawRaw markdown printed verbatim with aligned tables

Default: bat

[display]
render_mode = "raw"

display.show_session_id_on_create

Whether to display the session ID when a new session is created.

Default: false

display.show_session_id_on_exit

Whether to display the session ID when meka exits.

Default: true

[display]
show_session_id_on_create = true
show_session_id_on_exit = false

display.show_path_in_prompt

Whether to show the current working directory in the interactive prompt.

Default: true

display.newline_before_prompt

Whether to add a blank line before the prompt after each agent response.

Default: true

display.newline_after_prompt

Whether to add a blank line after the prompt (before the agent response).

Default: true

display.show_token_usage

When true, meka prints a one-line per-turn token-usage summary to stderr after each turn:

[in 12.3k / cache hit 96% / out 1.2k]

The in column is the total of all three Anthropic input tiers (live, cache-write, cache-read); cache hit % is cache_read / total_in. Useful for monitoring caching effectiveness during long sessions. The /status slash command surfaces cumulative session stats in the same vein.

Default: false

display.resume_show_recent

When set to a positive integer N, resuming a session reprints the last N turns (each turn = the user’s prompt plus everything the agent did in response, styled to match the live REPL) instead of just the last assistant message.

Useful when you regularly resume long-running sessions and want more context than the single-message default. Inside a session, the /history slash command provides the same rendering on demand (/history dumps everything; /history N shows the last N turns).

Default: unset (resume reprints only the last assistant message — today’s behaviour).

[display]
resume_show_recent = 3

display.input_style

Visual style applied to text typed into the REPL prompt. Makes submitted prompts easy to spot when scrolling back through a long session — reedline paints the buffer with this style on every repaint, including the final paint before the newline, so the styling lands in the terminal’s scrollback alongside the literal text.

Accepted values:

  • default (or unset): bold white-ish foreground on a slate-blue background, rendered in truecolor RGB so it looks the same across terminal themes.
  • none: disable styling entirely.
  • reverse: reverse video (swaps the terminal’s current foreground and background).
  • bold, dim, italic, underline: single attribute, no colour change.
  • A colour name (black, red, green, yellow, blue, magenta / purple, cyan, white): set only the foreground, mapped to the terminal’s palette.

Unknown values warn at startup and fall back to default.

Default: the banner preset described above.

[display]
show_path_in_prompt = false
newline_before_prompt = false
newline_after_prompt = false
input_style = "none"    # or "cyan", "bold", "dim", etc.

[web]

Settings for the HTTP client shared by fetch_url and web_search. All keys are optional; unset fields use the defaults shown below.

KeyTypeDefaultPurpose
user_agentstringReal Chrome UASome search engines block non-browser UAs. Override if you need a specific identifier.
request_timeout_secondsint30Total request budget (connect + TLS + read). 0 falls back to the default.
connect_timeout_secondsintunsetSeparate cap on TCP + TLS handshake. Fail fast on unreachable hosts without shortening the whole request budget.
read_timeout_secondsintunsetPer-chunk idle timeout. Catches bodies that stall mid-stream.
max_redirectsint10Cap on 3xx hops. 0 disables redirects entirely.
proxystringunset (honours HTTP_PROXY / HTTPS_PROXY / ALL_PROXY env)Proxy URL. Schemes: http://, https://, socks5://, socks5h://, socks4://. The literal string "none" explicitly disables env-var auto-detection.
ca_cert_filepathunsetExtra PEM bundle to trust on top of the system store. Useful for corporate MITM proxies or self-signed internal services. Accepts single-cert and multi-cert files.
https_onlyboolfalseRefuse plain http:// URLs.
min_tls_versionstringunset (reqwest default)Minimum TLS version. Accepts "1.0", "1.1", "1.2", "1.3". Unknown values log a warn and fall through. Note: the bundled rustls backend supports only TLS 1.2 and 1.3 — "1.0" / "1.1" will surface a build error.
danger_accept_invalid_certsboolfalseDANGEROUS. Disable TLS certificate validation entirely. Emits a warn! on every startup when enabled. Only use against trusted local dev servers.
danger_accept_invalid_hostnamesboolfalseDANGEROUS. Accept certificates whose hostname doesn’t match. Emits a warn! on every startup when enabled. Only use against trusted local dev servers.

Example: corporate proxy with a private CA

[web]
proxy = "http://corp-proxy.internal:3128"
ca_cert_file = "/etc/ssl/corp-root-ca.pem"
min_tls_version = "1.2"
request_timeout_seconds = 60

Example: local testing against self-signed certs

[web]
# Route everything through a local SOCKS proxy you control.
proxy = "socks5h://127.0.0.1:1080"
# Accept self-signed certs on dev.local — KEEP THIS OFF IN PROD.
danger_accept_invalid_certs = true

Example: fail-fast timeouts

[web]
request_timeout_seconds = 5
connect_timeout_seconds = 2
max_redirects = 0

[shell]

Settings for shell command execution.

shell.sandbox

Whether to enable read-only filesystem sandboxing for shell commands in read mode. When enabled (default), shell commands can be executed in read mode but with the filesystem physically write-protected. When disabled, shell commands require write mode.

Default: true

[shell]
sandbox = false  # disable sandboxed shell in read mode

The sandbox uses one of two backends on Linux (see shell.sandbox_backend), sandbox-exec on macOS, and a duplicated Low-integrity primary token on Windows. On platforms where no backend is usable, shell commands always require write mode regardless of this setting.

shell.sandbox_backend

Linux-only choice between "landlock" and "bubblewrap":

  • Bubblewrap ("bubblewrap") — wraps the command in bwrap with read-only bind of /, tmpfs masks over /run / /tmp / /var/tmp / $XDG_RUNTIME_DIR, and --unshare-user --unshare-pid --unshare-uts --unshare-ipc. The tmpfs masks hide the dbus session bus and the systemd-user socket, so state-changing IPC calls like systemctl --user start and dbus-send fail. Network is intentionally not unshared so curl http://x | pdftotext still works. Requires the bubblewrap package and a kernel with user-namespace creation enabled.
  • Landlock ("landlock") — uses the Landlock LSM (kernel 5.13+) to block filesystem writes. Does not block dbus / systemd-user IPC; a sandboxed shell can still invoke state-mutating dbus methods. Kept as the lighter-weight fallback for hosts without Bubblewrap.

When omitted, meka probes Bubblewrap once at startup. If Bubblewrap is available it auto-picks it; otherwise it auto-picks Landlock and emits a one-shot warning nudging you to install bubblewrap for stronger protection. Set the field explicitly to either value (including "landlock") to suppress that warning. meka setup does not write this field — leave it unset to keep auto-detection.

If the configured backend can’t be used at runtime (bwrap not installed, user namespaces denied, etc.), execute_command in read mode hard-errors with a message naming the configured backend and the specific failure reason. Read mode is not blocked for other tools — only execute_command requires a usable sandbox.

Default: unset (auto-detect). Ignored on macOS and Windows.

[shell]
sandbox = true
sandbox_backend = "bubblewrap"  # or "landlock"

[permissions]

Controls which permission modes are reachable at runtime and which mode the session starts in. See the Permissions page for what each mode does.

FieldRequiredDescription
defaultNoMode the session starts in. One of "none", "read", "ask", "write". Default "read". Overridden by --permission and MEKA_PERMISSION.
enabledNoList of modes that can be reached at runtime via /permission and Shift+Tab. Default ["none", "read", "write"]"ask" is opt-in. Disabled modes are skipped during Shift+Tab cycling and rejected by /permission with an error.

If default is not in enabled, meka logs a warning and falls back to read if it’s enabled, otherwise the lowest-discriminant enabled mode (in none → read → ask → write order). Same behavior if --permission or MEKA_PERMISSION selects a disabled mode — meka warns and starts in the configured default rather than refusing to launch.

[permissions]
default = "read"
enabled = ["none", "read", "ask", "write"]  # opt back into ask

[session]

Settings for session history retention and context window management.

session.context_messages

Maximum number of messages to send to the LLM API per request. Older messages are truncated from the beginning while preserving tool call chain integrity. The full history remains stored in SQLite – only the API payload is limited.

Default: 200

[session]
context_messages = 100

session.retention_days

Automatically delete sessions older than this many days on startup. Uses the session’s updated_at timestamp, so actively-resumed sessions are preserved even if originally created long ago.

Default: 90

[session]
retention_days = 30

session.max_storage_bytes

Maximum total byte size of all stored message content across all sessions. When exceeded on startup, the oldest sessions are deleted until the total is under the limit.

Default: 52428800 (50 MB)

[session]
max_storage_bytes = 10485760  # 10 MB

session.auto_compact

Automatically compact the conversation when input tokens exceed 80% of the context window. Compaction summarizes older messages and preserves recent ones, the todo list, and scratchpad entries.

Default: true

[session]
auto_compact = false

session.context_window

Override the model’s context window size (in tokens). Used for auto-compact threshold calculation. If not set, meka infers the context window from the model name.

[session]
context_window = 200000

[thinking]

Settings for extended thinking (claude-api and claude-oauth providers). Claude 4.6+ models use adaptive thinking automatically; older models use a fixed token budget.

thinking.enabled

Whether to enable extended thinking. When enabled, the model can use additional tokens for internal reasoning before responding.

Default: true

thinking.budget_tokens

Maximum number of tokens the model can use for thinking (for non-adaptive models).

Default: 16000

thinking.show_content

Whether to render thinking blocks inline in the terminal as the model produces them. When false, thinking is silently consumed (still sent on subsequent turns for cache continuity, just not displayed). When true, thinking deltas are streamed under a dimmed header.

Default: false

[thinking]
enabled = true
budget_tokens = 20000
show_content = true

[prompt]

Settings for injecting custom instructions into the system prompt. Use this to set installation-specific rules that should apply to every session – things the agent needs to know about your system, preferred tools, or policies.

prompt.instructions

A string of custom instructions that meka will include in every system prompt, under a ## User Instructions section. The model is told to treat them as hard constraints unless they conflict with safety requirements.

Suitable use cases:

  • System-specific policies: “Never install Python packages globally with pip – always use uv or a venv.”
  • Installed tooling the agent should know about: “Poppler is available on this system – use pdftotext for PDFs.”
  • Workflow preferences: “Prefer ripgrep over grep; it’s installed and faster.”
  • Signing / compliance rules: “Git commits on this system must use gpg signing.”

Default: unset (no custom instructions).

[prompt]
instructions = """
Never install Python packages globally with pip. Always use `uv` or a venv.
Poppler is available on this system — use `pdftotext` for PDFs.
Prefer ripgrep over grep.
"""

Notes:

  • Empty or whitespace-only strings are treated as unset.
  • Instructions apply to sub-agents spawned via spawn_agent too.
  • Instructions are included at all permission levels (including none) because they are authored by you.
  • Per-run override: --instructions (or MEKA_INSTRUCTIONS) replaces this value for a single invocation.

[mcp]

Settings for MCP (Model Context Protocol) tool servers. MCP allows meka to discover and use tools provided by external servers.

[[mcp.servers]]

An array of MCP server configurations. Each entry defines a server to connect to at startup.

FieldRequiredDescription
nameYesUnique name for this server. Used as namespace prefix for tools (name__tool). Must match [A-Za-z0-9_-]+, must not contain __, and must not be meka, ide, or start with mcp_.
transportYesTransport type: "stdio" (spawn subprocess) or "http" (streamable HTTP).
commandStdio onlyPath or name of the executable to spawn. On Windows, npx / .cmd / .bat / .ps1 are auto-wrapped in cmd /c.
argsNoArguments to pass to the command.
envNoEnvironment variables to set for the spawned process (stdio only).
urlHTTP onlyURL of the MCP server endpoint.
auth_tokenNoBearer token for HTTP authentication (sent as Authorization: Bearer <token>).
authNoOAuth authentication configuration (see below). Mutually exclusive with auth_token.
headersNoCustom HTTP headers to include with every request (HTTP only).
headers_helperNoPath to an executable whose stdout (Name: Value\n lines) is merged over headers at connect-time (HTTP only). Executed with MEKA_MCP_SERVER_NAME / MEKA_MCP_SERVER_URL in env; 15 s timeout.
permissionNoServer-wide permission override. Applies to every tool on this server, beating the readOnlyHint the server advertises and the [mcp].default_permission global fallback. See Permission resolution below.
allowed_toolsNoOptional allow-list of raw tool names (the form the server advertises, not the server__tool namespaced form). When set and non-empty, only these tools are registered; all others from this server are ignored.
disabled_toolsNoOptional block-list of raw tool names. Applied after allowed_tools — tools listed here are never registered. Both lists can coexist; the net set is allowed_tools \ disabled_tools.
eager_load_toolsNoRaw tool names that should ship eager-loaded instead of deferred. Listed tools skip the load_tool round-trip and sit in the cacheable tools-array prefix from turn 1. Use this for tools the agent invokes constantly (search, fetch, …); leave others deferred so the tools array stays lean.
tool_permissionsNoPer-tool permission overrides keyed by raw tool name. Beats the server-level permission and the server’s readOnlyHint when resolving a tool’s required permission.
samplingNoAllow this server to call sampling/createMessage against your configured LLM provider. Default false (reject). Enabling this lets a compromised server inject arbitrary messages into your LLM context and burn your provider quota — opt in per-server, deliberately.
sampling_limitNoCap on sampling calls per meka session from this server when sampling = true. Default 10. Requests beyond the limit return an INTERNAL_ERROR to the server.
disabledNoWhen true, the server is skipped entirely at startup — no process is spawned, no HTTP connect is attempted. Flip it back with meka mcp enable <name> or by editing the config. Defaults to false.

[mcp] top-level table

FieldPurpose
default_permissionFallback permission for MCP tools whose server didn’t advertise readOnlyHint and doesn’t have a permission override. Accepts "none", "read", "ask", or "write". If unset the hardcoded fallback is "write" (strict).
strictWhen true (default), every turn is gated on all enabled MCP servers being Connected. If any are not, the turn is rejected with a shell-style error instead of sending the request to the model. Set to false to proceed with whichever servers are ready (a warn log names the missing ones).
grace_secondsPer-turn cap on how long to wait for still-Pending servers to connect before applying the strict check. Default 3. Set to 0 to skip waiting (useful for scripts that want to fail fast).
connect_timeout_secondsPer-server timeout for connect + initialize + list_tools. A hung stdio spawn or slow HTTPS handshake can’t stall the whole fleet past this bound. Default 30.

Startup concurrency

MCP servers connect in parallel at startup, partitioned by transport so a fleet of stdio servers (process-spawn bound) doesn’t fight a fleet of HTTP servers (network bound):

  • stdio: MEKA_MCP_STDIO_CONCURRENCY (default 3)
  • http: MEKA_MCP_HTTP_CONCURRENCY (default 20)

These env vars are tuning knobs — rarely needed, but useful if you’re running ~30 stdio servers on a constrained box (lower it) or ~50 HTTP servers (raise it).

Permission resolution

Every MCP tool’s required permission is resolved through a five-step chain; the first match wins:

  1. server.tool_permissions[<raw-tool>] — explicit per-tool override.
  2. server.permission — explicit server-level override. Applies to every tool on that server regardless of what the server advertises.
  3. tool.annotations.readOnlyHint from the server: trueRead, falseWrite.
  4. [mcp].default_permission — global fallback.
  5. Hardcoded Write — strict ultimate fallback.

User-supplied config (1, 2, 4) always beats the server’s self-classification — if a server lies about a tool, you can override. But when no user config says anything, the server’s hint is trusted for that specific tool so readOnlyHint = false destructive tools don’t silently become Read-accessible just because the user opted into a lenient global default.

Hint spoofing: a compromised server could claim readOnlyHint = true on a destructive tool. Defend by setting server.permission = "write" on suspect servers (step 2 wins) or by listing the destructive tools explicitly in tool_permissions / disabled_tools.

Stale config: entries in allowed_tools / disabled_tools / eager_load_tools / tool_permissions that don’t match any advertised tool get a warn! line at connect time. The server still connects; you just see a heads-up so you can clean up after the server renames a tool. A name that appears in both eager_load_tools and disabled_tools also warns — the disabled filter wins, so eager-loading the disabled tool is a no-op.

Visibility across levels: the resolved permission doesn’t hide a tool from the agent. Every registered tool is listed in the system prompt with its required level noted inline, and a per-turn [Permission context] block names the current level plus any tools it blocks. The agent can still reason about an inaccessible tool and suggest /permission <level> to enable it; the permission gate is enforced at dispatch time. Keeping the tool catalogue visible across levels is also what lets the Claude prompt cache survive mid-session permission toggles.

Examples

Exa — reliable web search when the built-in DuckDuckGo scraper gets CAPTCHA’d. The free tier works without an API key; paste a key into the headers table for the paid tier:

# Free tier — no key required
meka mcp add exa https://mcp.exa.ai/mcp
# Paid tier — expands from EXA_API_KEY at connect time
meka mcp add exa https://mcp.exa.ai/mcp --header "x-api-key=${EXA_API_KEY}"

Well-annotated server — no config needed. Every tool is classified by its own readOnlyHint (read tools Read, write tools Write):

[[mcp.servers]]
name = "notion"
transport = "http"
url = "https://mcp.notion.com/mcp"

User-declared trust on an unannotated server — all tools accessible in Read:

[[mcp.servers]]
name       = "internal"
transport  = "http"
url        = "https://mcp.internal/…"
permission = "read"

Overriding a mis-annotated or distrusted tool — one specific tool requires Write:

[[mcp.servers]]
name      = "notion"
transport = "http"
url       = "https://mcp.notion.com/mcp"

[mcp.servers.tool_permissions]
"notion-do-something-scary" = "write"

Subset of a server’s tools — only query registers, all others are ignored:

[[mcp.servers]]
name          = "pg"
transport     = "stdio"
command       = "npx"
args          = ["-y", "@modelcontextprotocol/server-postgres"]
allowed_tools = ["query"]

Block-list with a narrow exception — all fs tools are Read-accessible except the two destructive ones, which are never registered:

[[mcp.servers]]
name           = "filesystem"
transport      = "stdio"
command        = "npx"
args           = ["-y", "@modelcontextprotocol/server-filesystem"]
permission     = "read"
disabled_tools = ["delete_file", "move_file"]

MCP tools are registered with namespaced names in the format servername__toolname to prevent collisions with built-in tools or between servers.

Tool and resource descriptions returned from MCP servers are truncated at 2048 characters to keep the system prompt bounded.

Environment variable substitution

Every string field listed above (command, args, env values, url, headers values, auth_token) supports ${VAR} and ${VAR:-default} expansion from the process environment. Missing variables with no default leave the literal ${VAR} in place and log a warning at startup. Use this to avoid committing secrets:

[[mcp.servers]]
name = "github"
transport = "http"
url = "https://mcp.github.com"
auth_token = "${GITHUB_MCP_TOKEN}"

Environment variables

VariableDefaultPurpose
MEKA_MCP_TOOL_TIMEOUT600000 ms (600 s)Per-call timeout for MCP tools. Triggers notifications/cancelled on expiry.

meka mcp CLI

Manage configured servers without editing config.toml by hand:

CommandAction
meka mcp listPrint all configured servers.
meka mcp get <name>Print full details for one server.
meka mcp add <name> <url-or-command> [args...] [flags]Persist a server. Transport is auto-detected: a URL starting with http[s]:// means HTTP, anything else means stdio. Preserves existing formatting/comments via toml_edit.
meka mcp remove <name>Best-effort revoke stored OAuth tokens (RFC 7009) at the provider, then delete the server entry, clear stored credentials, and drop any resource-update ledger entries.
meka mcp disable <name>Set disabled = true on the server entry. The next meka start skips it entirely.
meka mcp enable <name>Clear the disabled flag, so the server connects on the next start.
meka mcp reconnect <name>Smoke-test a connect; prints ok or the error.
meka mcp tools <name>Connect and list every advertised tool with its resolved permission, the chain step that decided it, and whether the current config allows it. Useful for populating --allow-tool, --disable-tool, or --tool-permission overrides without leaving the CLI.
meka mcp login <name>Drive interactive OAuth. If the server has no [auth] block and uses HTTP, assumes type = "oauth" and persists the block on success.
meka mcp logout <name>Call the provider’s revocation_endpoint (RFC 7009) best-effort, then clear stored credentials + auth-probe cache.

meka mcp add flags

FlagPurpose
--transport <stdio|http>Override the auto-detected transport.
--env KEY=VALUEEnvironment variable for stdio (repeatable).
--header KEY=VALUEHTTP header (repeatable).
--auth <oauth|client-credentials|client-credentials-jwt>Configure the [auth] block.
--auth-token <TOKEN>Static bearer token. Mutually exclusive with --auth.
--client-id, --client-secretOAuth / client-credentials client identifiers.
--signing-key <PATH>, --signing-algorithm <ALG>JWT signing material (client-credentials-jwt only).
--scope <SCOPE>OAuth scope (repeatable).
--redirect-port <PORT>Fixed OAuth redirect port (default: ephemeral).
--permission <none|read|ask|write>Per-server permission cap (applies to all tools on the server).
--allow-tool <NAME>Raw tool name to allow (repeatable). When set, only listed tools register.
--disable-tool <NAME>Raw tool name to block (repeatable). Applied after --allow-tool.
--eager-load-tool <NAME>Raw tool name to eager-load (repeatable). Listed tools skip the load_tool round-trip and ship in the cacheable tools-array prefix from turn 1.
--tool-permission <NAME=LEVEL>Per-tool permission override (repeatable). LEVEL is none/read/ask/write.
--sampling, --sampling-limit <N>Opt into server-initiated sampling/createMessage.

Example: Notion

$ meka mcp add notion https://mcp.notion.com/mcp
ok: added 'notion' to ~/.config/meka/config.toml
probe: server requires OAuth.
running OAuth authorisation for 'notion' (use --no-login to skip).
no [auth] block for 'notion' — assuming OAuth authorization_code.
…
ok: authorized 'notion'

meka mcp add on an HTTP endpoint:

  1. Probe — issues an unauthenticated GET (3 s timeout, redirects off) and classifies the response per the MCP authorization spec + RFC 6750 + RFC 9728:

    • 2xx → server is open, no login needed.
    • 401 / 403 with WWW-Authenticate: Bearer … → OAuth required. The resource_metadata="…" attribute (RFC 9728) is captured at DEBUG.
    • Any other status → couldn’t infer, prints the status code.
    • Network failure → prints the error.
  2. Auto-login — if the probe says OAuth is required (or --auth oauth was explicitly set), the OAuth authorization_code flow runs immediately as though the user had chained meka mcp login <name> themselves. The synthesised [auth] = oauth block is written back to config.toml on success.

  3. Rollback on failure — if the OAuth flow errors out, the entry we just wrote is purged from config.toml (alongside any partial credentials + probe cache), leaving the user’s config clean. The command exits non-zero.

  4. --no-login — skips step 2. The entry is still persisted and the probe’s hint is still printed; run meka mcp login <name> when ready. Useful for scripted setup or when you expect to edit [auth] by hand.

The probe and the auto-login only run for HTTP servers, and only when the user didn’t provide --auth-token (static bearer) or --auth (other than oauth). Stdio servers skip both.

Remote hosts / SSH sessions

The OAuth flow redirects the browser to http://127.0.0.1:<port>/callback. When meka is running on a different host than the browser (SSH session, container, Codespace, WSL), the browser can’t reach back and shows a “connection refused” error page. meka handles this automatically:

  • While meka mcp login <name> waits for the callback it also watches stdin.
  • The browser’s address bar still contains the full callback URL (including code and state) even when the connection fails. Copy it, paste it into the meka prompt, and press Enter.
  • Whichever completes first — the TCP callback or the pasted URL — wins.
$ meka mcp login notion
server 'notion' has no [auth] block; assuming OAuth authorization_code.
Opening browser for MCP server 'notion' OAuth authorization...
If the browser didn't open, visit:
  https://mcp.notion.com/authorize?response_type=code&…
Waiting for OAuth callback (up to 120s).
  If the browser can't reach this host (e.g. you're over SSH), paste the full
  callback URL here and press Enter.
http://127.0.0.1:46437/callback?code=…&state=…     ← paste here
ok: authorized 'notion'

REPL parity

Inside the REPL:

  • /mcp list — list configured servers.
  • /mcp reconnect <server> — reconnect smoke-test.
  • /mcp login <server> / /mcp logout <server> — run the auth flow or revoke.
  • /mcp <server>:<prompt> [args...] — render a server-defined prompt as the next user turn.

Resources and prompts

In addition to tools, meka exposes MCP resources and prompts through several builtin tools (deferred — the agent calls load_tool first to fetch the schema, then invokes them):

BuiltinPurpose
list_mcp_resourcesList resources from one or every configured server.
read_mcp_resourceRead a resource by server + uri; text inline, binary base64-encoded.
list_mcp_promptsList prompts from one or every configured server, including their declared arguments.
get_mcp_promptRender a prompt by server + name with optional arguments; returns <role>: <text> lines.
subscribe_mcp_resourceSubscribe to resources/updated notifications for a specific URI.
unsubscribe_mcp_resourceCancel a prior subscription.
list_mcp_resource_updatesPrint every resource that has been reported as updated since the session started.

Connection lifecycle

  • Reconnection is automatic for all transports (stdio, plain HTTP, OAuth-authenticated HTTP) when the transport closes mid-session. HTTP transports use exponential backoff (1s, 2s, 4s, 8s, 16s, capped 30s, max 5 attempts); stdio gets one immediate retry. The reconnect runs on a blocking thread to work around an upstream rmcp bug where the auth future is !Send.
  • Session-expired recovery: rmcp 1.5 transparently re-initialises HTTP sessions on 404 / JSON-RPC -32001. meka relies on this; no per-call handling is required.
  • Cancellation: when the agent cancels a tool call (e.g. Ctrl-C), meka sends notifications/cancelled to the server with the in-flight request id so the server can stop work.
  • Timeouts: tool calls default to 600 s; override with MEKA_MCP_TOOL_TIMEOUT in ms.
  • Tool list refresh: on tools/list_changed, meka re-discovers the server’s tools and hot-swaps them in the registry — no restart needed.
  • Progress notifications: MCP tool calls attach a per-request progressToken; incoming notifications/progress render as a live status line under the tool invocation.
  • Server instructions: InitializeResult.instructions is captured once per connection and spliced into the system prompt (sanitised + truncated to 2048 chars) under ## MCP Server Instructions.
  • Auth-probe cache: 401 responses are cached for 15 minutes so a restart after a failed auth flow skips the unauthenticated probe and goes straight to OAuth. Cleared by meka mcp logout.
  • resources/list_changed, prompts/list_changed, and resources/updated notifications are logged at info/debug level.

Server-to-client features

Featuremeka behaviour
roots/listReturns a single root: file://<current-working-directory> with the directory basename as the name.
elicitation/createAlways responds with Decline and logs a warning — interactive form/URL input is not wired into the REPL.
sampling/createMessageRejected with METHOD_NOT_FOUND unless the server has sampling = true in its config. When allowed, the current provider handles the request; per-session sampling_limit caps how many times each server may invoke it.

[mcp.servers.auth]

OAuth authentication for HTTP MCP servers. Set type to choose the authentication method. This is mutually exclusive with auth_token.

FieldRequiredDescription
typeYesAuth method: "client_credentials", "client_credentials_jwt", or "oauth"
client_idVariesOAuth client ID (required for client_credentials/jwt, optional for oauth with dynamic registration)
client_secretVariesClient secret (required for client_credentials, optional for oauth)
scopesNoOAuth scopes to request
resourceNoResource parameter (RFC 8707), client_credentials only
signing_key_pathJWT onlyPath to PEM private key file
signing_algorithmNoJWT signing algorithm: RS256 (default), RS384, RS512, ES256, ES384
redirect_portNoLocal port for OAuth authorization code callback. When omitted, meka binds to a random ephemeral port (recommended). oauth only.

Examples

Stdio server

[[mcp.servers]]
name = "postgres"
transport = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]
permission = "write"

HTTP server

[[mcp.servers]]
name = "web-tools"
transport = "http"
url = "http://localhost:8080/mcp"
permission = "read"

HTTP server with authentication

[[mcp.servers]]
name = "api"
transport = "http"
url = "https://api.example.com/mcp"
auth_token = "your-bearer-token"
permission = "write"

[mcp.servers.headers]
X-Custom-Header = "value"

Stdio server with environment variables

[[mcp.servers]]
name = "github"
transport = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
permission = "read"

[mcp.servers.env]
GITHUB_TOKEN = "ghp_..."

Multiple servers

[[mcp.servers]]
name = "filesystem"
transport = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
permission = "read"

[[mcp.servers]]
name = "github"
transport = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
permission = "write"

HTTP server with OAuth client credentials

[[mcp.servers]]
name = "api"
transport = "http"
url = "https://api.example.com/mcp"
permission = "write"

[mcp.servers.auth]
type = "client_credentials"
client_id = "my-client-id"
client_secret = "my-client-secret"
scopes = ["read", "write"]

HTTP server with JWT client credentials

[[mcp.servers]]
name = "api"
transport = "http"
url = "https://api.example.com/mcp"

[mcp.servers.auth]
type = "client_credentials_jwt"
client_id = "my-client-id"
signing_key_path = "/path/to/private-key.pem"
signing_algorithm = "RS256"
scopes = ["admin"]

HTTP server with OAuth authorization code flow

On first connection, meka opens a browser for authorization and stores the token for future use.

[[mcp.servers]]
name = "github-mcp"
transport = "http"
url = "https://mcp.example.com"

[mcp.servers.auth]
type = "oauth"
client_id = "my-app-id"
scopes = ["repo", "user"]
redirect_port = 8400

If client_id is omitted, meka attempts dynamic client registration with the server.

[tools] — built-in tool filters

The three knobs [[mcp.servers]] exposes for MCP tools also apply to meka’s built-in tools (read_file, write_file, execute_command, web_search, etc.) via a top-level [tools] table. MCP per-server filtering is separate from this and keeps its own namespaces — this block only affects the built-ins.

KeyPurpose
allowed_toolsOptional allow-list of built-in tool names. When set and non-empty, only these built-ins register. Use meka tools list to see the canonical names.
disabled_toolsBlock-list of built-in tool names. Applied after allowed_tools; a tool here is never registered even if it also appears in the allow-list.
tool_permissionsPer-tool required-permission override keyed by built-in name. Beats the hardcoded required level from the tool’s impl. Levels: none, read, ask, write.

Stale entries (a name that doesn’t match any built-in) emit a warn! at startup. meka still starts — the warning just flags a likely typo or a tool the binary renamed.

Restrict a session to read-only inspection:

[tools]
allowed_tools = ["read_file", "find_files", "search_contents", "fetch_url"]

Force execute_command to need write so ask mode prompts for every shell call:

[tools.tool_permissions]
execute_command = "write"

Disable web access entirely in a locked-down environment:

[tools]
disabled_tools = ["web_search", "fetch_url"]

Sub-agents spawned via spawn_agent inherit the same filter — a disabled built-in is disabled everywhere. Run meka tools list to see every built-in’s effective required permission, whether a [tools.tool_permissions] override is in effect, and whether the current config enables it.

[serve]

Configuration for meka serve, the HTTP API server. See the HTTP API usage guide for a full walkthrough.

serve.bind

Address and port the HTTP server listens on.

TypeDefault
string"127.0.0.1:8080"
[serve]
bind = "0.0.0.0:8080"

Security: Binding to 0.0.0.0 exposes the server on all interfaces. In production, keep 127.0.0.1 and front with a TLS-terminating reverse proxy.

serve.max_body_bytes

Maximum request body size in bytes. Requests exceeding this limit are rejected with 413 Payload Too Large.

TypeDefault
integer10485760 (10 MiB)

serve.max_concurrent_turns

Process-wide cap on in-flight turns across all sessions. When the cap is reached, new turn submissions return 429 Too Many Requests with a Retry-After header. Unset or 0 means no limit.

TypeDefault
integerunbounded

serve.idle_timeout

How long a session can sit idle (no turns submitted) before the GC evicts it from memory. Accepts duration strings like "24h", "30m", "7d". Set to "0" to disable idle GC.

TypeDefault
string (duration)"24h"

Eviction drops the in-memory runtime but preserves the SQLite row — a later request transparently re-attaches. See delete_on_idle to also remove the DB row.

serve.gc_scan_interval

How often the background GC scanner runs. Accepts duration strings.

TypeDefault
string (duration)"5m"

serve.delete_on_idle

When true, idle-evicted sessions also have their SQLite row deleted. When false (default), only the in-memory state is dropped and the session can be re-attached later.

TypeDefault
boolfalse

serve.shutdown_drain_timeout

Maximum time to wait for in-flight turns and tasks to finish during graceful shutdown (SIGTERM / SIGINT). After this timeout, remaining tasks are aborted and the process exits.

TypeDefault
string (duration)"30s"

[[serve.tokens]]

An array of bearer tokens for API authentication. At least one token is required.

KeyRequiredDescription
tokenYes*The bearer token value. Supports ${ENV_VAR} substitution. Mutually exclusive with token_file.
token_fileYes*Path to a file containing the token (one line, trimmed). Mutually exclusive with token. A startup warning is logged if the file is world-readable.
descriptionNoHuman-readable label for this token (appears in logs).
scopesYesArray of scope strings: "sessions:r", "sessions:w", "skills:r", "mcp:r".

* Exactly one of token or token_file must be set.

Inline plaintext tokens log a startup warning — use ${ENV_VAR} or token_file for production.

Examples

Development token (inline):

[[serve.tokens]]
token = "sk_dev_test123"
scopes = ["sessions:r", "sessions:w"]

Production token (environment variable):

[[serve.tokens]]
token = "${MEKA_BRIDGE_TOKEN}"
description = "telegram bridge"
scopes = ["sessions:r", "sessions:w"]

Production token (file-based):

[[serve.tokens]]
token_file = "/etc/meka/bridge.token"
description = "telegram bridge"
scopes = ["sessions:r", "sessions:w"]

Admin token with all read scopes:

[[serve.tokens]]
token = "${MEKA_ADMIN_TOKEN}"
description = "operator"
scopes = ["sessions:r", "sessions:w", "mcp:r", "skills:r"]

Environment Variables

The config file is the recommended way to configure meka. Environment variables are useful as overrides – for example, in CI pipelines, containers, or when you want to temporarily switch providers without editing your config.

Environment variables override config file values but are overridden by CLI flags.

meka-Specific Variables

VariableDescriptionExample
MEKA_PROVIDERLLM provider nameopenai-api, claude-api, claude-oauth
MEKA_MODELModel identifiergpt-4o, claude-sonnet-4-20250514
MEKA_PERMISSIONDefault permission modenone, read, write
MEKA_INSTRUCTIONSReplace [prompt].instructions for this run. Equivalent to --instructions. Used by the mekabox container wrapper to tell the agent it can install packages freely.Be terse.
MEKA_CONFIG_DIROverride the default config directory. Points at the meka directory itself (contains config.toml and skills/). The only isolation knob that works on every platform — dirs::config_dir() ignores $XDG_CONFIG_HOME on macOS/Windows./tmp/meka-test/meka
MEKA_DATA_DIROverride the default data directory (where sessions.db lives). Same cross-platform escape hatch — dirs::data_dir() ignores $XDG_DATA_HOME on macOS/Windows. Useful for tests, portable installs, and per-project session isolation./tmp/meka-test/data/meka

MCP Variables

VariableDescriptionDefault
MEKA_MCP_TOOL_TIMEOUTPer-call timeout for MCP tools, in milliseconds. Applies to every remote tool invocation; on timeout meka cancels the request and returns an error to the model.600000 (600s)

Provider API Keys

VariableUsed When
OPENAI_API_KEYProvider is openai-api
CLAUDE_API_KEYProvider is claude-api

OAuth Authentication

VariableDescription
CLAUDE_OAUTH_TOKENOAuth access token for the claude-oauth provider
OPENAI_CODEX_TOKENOAuth access token for the openai-codex provider
CODEX_CLIENT_IDOverride the default OpenAI OAuth client ID for the openai-codex setup wizard (rarely needed)

On first use, the OAuth token is saved to the database and loaded automatically on subsequent launches. Setting the env var again replaces the stored token.

Provider Base URL

VariableDescription
OPENAI_BASE_URLCustom base URL for the OpenAI-compatible endpoint

Logging

meka uses the tracing framework. The log level can be controlled with:

VariableDescriptionExample
RUST_LOGStandard Rust log filtermeka=debug, meka=trace

If RUST_LOG is not set, the verbosity flag (-v, -vv, -vvv) controls the level:

FlagLevel
(none)warn
-vinfo
-vvdebug
-vvvtrace

Logs are written to stderr so they do not interfere with agent output.

CLI Options

meka [OPTIONS] [PROMPT]
meka <COMMAND>

Commands

setup

Run the interactive configuration wizard. Prompts for provider, authentication, model, and base URL, then writes the configuration to ~/.config/meka/config.toml.

meka setup

This wizard also runs automatically on first launch when no config file exists.

export

Export a session as Markdown.

meka export <SESSION_ID> [-o <PATH>]

Use -o - to print to stdout. See Sessions for details.

delete

Delete one or more sessions by UUID, or all sessions with --all.

meka delete <SESSION_ID>...
meka delete --all

list

List past sessions with ID, last update time, and a preview.

meka list [-n <LIMIT>]

Default limit: 20.

Arguments

[PROMPT]

Run the agent’s first turn immediately with this text as the user message, then drop into the interactive REPL for follow-up. Pair with --oneshot to exit after the first turn instead of opening the REPL.

meka "list all files larger than 1MB in the current directory"   # first turn, then REPL
meka --oneshot "list all files larger than 1MB"                  # first turn, then exit

When omitted, meka starts the REPL with no initial input.

Options

-c, --continue [SESSION_ID]

Resume a session. Without a session ID, resumes the most recently updated session. With a session ID, resumes that specific session — accepts either the full UUID or any unique leading prefix (most-recent-first if the prefix matches more than one).

meka -c                                          # resume last session
meka -c 550e8400-e29b-41d4-a716-446655440000     # full UUID
meka -c 550e                                     # prefix; works if unique

Errors if the session does not exist, the prefix matches multiple sessions (with the matching IDs listed for disambiguation), or the session is locked by another meka instance.

--permission <MODE>

Set the initial permission mode. Accepts none (or n), read (or r), ask (or a), write (or w).

meka --permission write
meka --permission ask

Default: read.

--provider <NAME>

Set the LLM provider. Overrides MEKA_PROVIDER and the config file.

meka --provider claude-oauth

Supported values: openai-api, claude-api, claude-oauth.

-m, --model <MODEL>

Set the model name. Overrides MEKA_MODEL and the config file.

meka -m gpt-4o-mini

--base-url <URL>

Set a custom API base URL. Overrides OPENAI_BASE_URL and the config file.

meka --base-url http://localhost:11434/v1

--no-stream

Disable streaming mode. The agent waits for the complete response before displaying it. By default, responses are streamed token-by-token.

meka --no-stream

--render-mode <MODE>

Set the output render mode. Accepts bat (default), termimad (or rich), or raw.

  • bat: Syntax-highlighted markdown output via bat.
  • termimad: Full terminal formatting (box-drawn code blocks, reflowed paragraphs, formatted tables).
  • raw: Raw markdown printed verbatim with aligned tables.
meka --render-mode raw

Can also be set permanently via display.render_mode in the config file.

--thinking

Enable extended thinking (claude-api and claude-oauth providers).

meka --thinking

--thinking-budget <TOKENS>

Set the extended thinking token budget. Implies --thinking.

meka --thinking-budget 20000

--instructions <STRING>

Override prompt.instructions for this run, replacing whatever is in the config file. Also reads from MEKA_INSTRUCTIONS if the flag is not given.

meka --instructions "Be terse. No code fences in answers."

--skill <NAME>

Invoke a skill as the first turn. Mirrors the REPL slash command /skill <name> [extra...] — the positional [PROMPT] arg, if given, is prepended to the rendered skill body as additional context. Pair with --oneshot to exit after the turn instead of opening the REPL.

meka --skill download-videos "https://example.com/video"             # first turn, then REPL
meka --skill download-videos --oneshot "https://example.com/video"   # first turn, then exit

Errors out with a clean message if the skill name is unknown.

--oneshot

Exit after the first turn finishes. Requires either the positional [PROMPT] or --skill <NAME> — without one of those, meka has nothing to do. Useful for scripts and CI invocations.

meka --oneshot "summarize the last commit"
meka --oneshot --skill deploy "to staging"

--eager-load-tool <SERVER:TOOL>

Eager-load a specific MCP tool for this session, bypassing the load_tool round-trip. The tool’s schema ships in the cacheable tools-array prefix from turn 1 instead of being deferred. Mirrors the per-server eager_load_tools config field — repeatable, raw tool names (the server-advertised form, not mcp__<server>__<tool>).

Particularly useful for scripted runs that know up front which tools they’ll need. The flag appends to whatever eager_load_tools lists in config.toml for that server — it doesn’t replace existing entries. Unknown server names log a warning and are skipped.

meka --eager-load-tool notion:search --eager-load-tool github:create_issue \
     --oneshot "search Notion for the deploy runbook and open a GitHub issue"

-v, --verbose

Increase log verbosity. Can be repeated up to three times.

meka -v      # info
meka -vv     # debug
meka -vvv    # trace

--help

Print help information.

--version

Print version information.

Interactive Mode

Start meka without the -p flag to enter interactive mode:

meka

You get a prompt:

meka [r] >

Type your instruction and press Enter to submit. The agent processes your request and prints its response (streamed in real time as Markdown). When it finishes, you get another prompt.

Keybindings

meka uses Emacs-style keybindings (provided by reedline).

Input

KeyAction
EnterSubmit the current prompt
Alt+EnterInsert a newline (for multi-line input)
Shift+TabCycle the permission mode (none → read → ask → write → none)
KeyAction
Ctrl+AMove cursor to start of line
Ctrl+EMove cursor to end of line
Ctrl+FMove cursor forward one character
Ctrl+BMove cursor backward one character
Alt+FMove cursor forward one word
Alt+BMove cursor backward one word

Editing

KeyAction
Ctrl+DDelete character under cursor / exit on empty line
Ctrl+H, BackspaceDelete character before cursor
Ctrl+KKill text from cursor to end of line
Ctrl+UKill text from start of line to cursor
Ctrl+WKill word before cursor
Ctrl+YYank (paste) killed text

Control

KeyAction
Ctrl+CInterrupt the running agent; clear the line if idle
Ctrl+DExit the shell (when the line is empty)
Ctrl+RReverse incremental search through history
Ctrl+LClear the screen

Prompt Format

meka [indicator] >

The indicator shows the current permission mode:

ModeIndicatorColor
None[n]Green
Read[r]Yellow
Ask[a]Magenta
Write[w]Red

The color provides a visual cue about the agent’s current capabilities. Red means the agent can modify your system.

Multi-Line Input

Press Alt+Enter to insert a newline instead of submitting. The prompt changes to show continuation:

meka [r] > write a python script that
  ... prints hello world
  ... and saves it to hello.py

Press Enter on the last line to submit the entire multi-line input.

Pasting multi-line content also works seamlessly — all pasted lines appear in the buffer for review, and you press Enter to submit.

Slash Commands

meka supports / prefix commands for controlling the shell:

CommandDescription
/helpShow available commands
/exitExit the shell
/clearClear the terminal screen
/sessionShow the current session ID
/permission [none|read|ask|write]Show or set the permission level
/compactSummarize and compact the session history
/cd [path]Change working directory
/mcp listList configured MCP servers with their live state (pending / connected / failed / disabled)
/mcp reconnect <server>Smoke-test connect for one server
/mcp login <server>Run the OAuth flow from the REPL
/mcp logout <server>Revoke cached credentials for a server
/mcp <server>:<prompt> [args...]Render a server-defined prompt and send it to the agent
/statusShow cumulative session stats (turns, tokens, cache hit ratio, redactions, message count)
/history [N]Reprint past conversation styled like the live REPL. Bare /history dumps everything; /history N shows the last N turns

/history

Replays prior messages in the current session so you can scroll back through context without exiting and re-resuming. /history with no argument dumps every materialised message; /history 5 shows the last 5 turns (a turn = the user’s prompt plus everything the agent did to respond). Any non-numeric argument (/history all, /history foo) falls back to the dump-everything path.

The renderer mimics the live REPL: assistant text flows through the same markdown highlighter, tool calls appear as [tool ReadFile(...)] indicators, and thinking blocks honour [thinking].show_content. User prompts are prefixed with a cyan > so they stand out from agent text.

For users who always want extra context at resume time, set display.resume_show_recent — the resume code path then renders the last N turns through the same function.

/status

Print a snapshot of the current session’s counters:

Session status
  Turns:           23
  Input tokens:    234.5k  (cache hit: 92%)
  Output tokens:   12.1k
  Redactions:      2 (12 images, ~38 MiB freed)
  Messages:        47

Redactions reports any times the Claude provider had to drop oldest tool-result image blocks because the request body would have exceeded Anthropic’s 32 MiB ceiling. A non-zero count indicates the cache prefix was invalidated for the redacted messages. See display.show_token_usage for a per-turn variant of the same data.

/compact

The /compact command asks the LLM to summarize the entire conversation, then replaces the messages the model sees with a single summary message followed by the recent tail. This is useful for long sessions that are approaching the context window limit or becoming expensive.

After compacting, the session continues with the summary as context. The pre-compaction messages stay in the underlying event log on disk — the model just no longer sees them — so meka export and resume continue to work exactly as if the compaction had wiped them.

Shell Escape

Prefix any input with ! to execute it directly as a shell command, bypassing the LLM entirely:

meka [r] > !pwd
/home/user/projects
meka [r] > !ls -la
total 32
drwxr-xr-x  5 user user 4096 Mar  4 10:00 .
...
meka [r] > !ping 1.1.1.1 -c 2
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
...

The command runs with inherited stdin/stdout/stderr, so it behaves exactly like a regular shell. This is useful for quick checks without waiting for the LLM.

Exiting

You can exit meka in any of these ways:

  • Type /exit
  • Type exit or quit
  • Press Ctrl+D on an empty line

Interrupting the Agent

Press Ctrl+C while the agent is running to interrupt it. This cancels the current LLM request and kills any running shell commands that were spawned by the agent.

One-Shot Mode

One-shot mode runs a single prompt and exits, similar to bash -c:

meka "your prompt here"

The agent processes the prompt (including any tool calls), prints its response, and the process terminates. The session UUID is printed to stderr on exit.

Examples

# Simple question
meka "what is my current working directory?"

# File operations (requires write permission)
meka --permission write "create a file called notes.txt with today's date"

# Search
meka "find all TODO comments in this project"

# Web search
meka "search the web for the latest Rust release"

Combining with Other Flags

All configuration flags work in one-shot mode:

# Use a specific provider and model
meka --provider claude-oauth -m claude-sonnet-4-20250514 "explain this codebase"

# With write permission
meka --permission write "run 'cargo test' and summarize the results"

# Disable streaming
meka --no-stream "read README.md and summarize it"

Session Behavior

One-shot mode creates a new session for each invocation. The session UUID is printed to stderr when the run completes:

Session: 550e8400-e29b-41d4-a716-446655440000

You can resume this session later in interactive mode:

meka -s 550e8400-e29b-41d4-a716-446655440000

ACP (Agent Client Protocol)

meka acp speaks the Agent Client Protocol over stdio so editor / web / messenger clients can drive an meka turn end to end. Where Interactive Mode and One-Shot Mode are for humans, ACP is for programs that want to host meka inside a richer UI — streamed diffs, native apply-buttons, hosted terminals, slash-command palettes.

This page describes what meka’s ACP surface looks like to a client. Editor-specific setup belongs in each editor’s own documentation; the protocol contract is the same everywhere.

Starting an ACP server

meka acp

The process speaks JSON-RPC 2.0 with newline framing on stdio. There is no human-facing prompt — the binary is meant to be spawned by a client that owns the conversation. The client sends initialize, then session/new (or session/load / session/resume), then a series of session/prompt calls.

A few flags are worth knowing:

FlagEffect
-vLogs to stderr at info (incoming client identity, session lifecycle).
-vvdebug (per-request JSON-RPC diagnostics).
RUST_LOG=meka=traceTrace level.

On startup, after the client’s initialize arrives, meka logs ACP client connected: <name> <version> so you can confirm the client identity in -v mode.

What meka advertises (agentCapabilities)

These are returned in InitializeResponse.agentCapabilities:

  • loadSession: true — the client may call session/load with any persisted session id.
  • sessionCapabilities.list — the client may call session/list to browse the persisted session catalogue (cwd-filtered, cursor-paginated; sub-agent audit sessions are hidden).
  • sessionCapabilities.resume — the client may adopt a persisted session id without replaying history.
  • sessionCapabilities.close — the client may release the active session slot.

mcpCapabilities is intentionally not advertised. meka is itself an MCP client, but the servers it consumes are configured via meka’s own config.toml rather than the mcpServers field on session/new. Advertising HTTP/SSE while silently ignoring the client’s array would have been misleading; the marker will return when client-supplied MCP server connections are actually implemented.

agentInfo carries meka’s name ("meka") and the running binary version.

What meka consumes (clientCapabilities)

The client advertises these in InitializeRequest.clientCapabilities; meka stashes them and lets the built-in tools route accordingly:

  • fs.readTextFile: trueread_file issues fs/read_text_file { sessionId, path, line?, limit? } so the client serves the in-buffer view of the file. Image and regex read_file modes have no fs/* analogue and stay local.
  • fs.writeTextFile: truewrite_file and edit_file’s apply step issue fs/write_text_file { sessionId, path, content }. meka still attaches diff metadata to the tool_call_update so clients with an apply-diff UI can render it.
  • terminal: trueexecute_command runs the four-call dance: terminal/createterminal/wait_for_exitterminal/outputterminal/release. On session/cancel or a per-call timeout, meka issues terminal/kill and still reads accumulated output. Exception: in read permission mode meka keeps the local sandboxed shell (Landlock / bwrap / sandbox-exec / Low-Integrity) rather than delegating, so the sandbox isn’t bypassed by the client’s terminal.

If the client omits a capability, the matching tool falls back to local syscalls — the user-visible behaviour is the same as meka in the REPL.

Session lifecycle

meka holds an in-memory map of sessionId → SessionEntry. Any number of sessions can coexist in one meka acp process, each with its own cwd, permission level, conversation, cancellation token, and per-session runtime mutex. Prompts on different sessions run in parallel; a second session/prompt for a session that already has one in flight is rejected with InvalidParams. The session row is also locked on disk (the same lock the REPL uses), so two meka processes can’t simultaneously write events for the same session id.

  • session/new { cwd, mcpServers } — mints a fresh persisted session, captures the cwd, takes the on-disk session lock, returns the session id and the current SessionMode state.
  • session/load { sessionId, cwd, mcpServers } — replays the persisted conversation as a stream of session/update notifications (user_message_chunk, agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update) before the response. Orphan tool calls (the persisted log stopped mid-tool) are closed out with a failed status so the client’s UI doesn’t render a stuck spinner. If the client’s cwd differs from the persisted one, meka updates the persisted row to match — the client wins.
  • session/list { cwd?, cursor? } — paginated index. Filtered to the requested cwd when present; sub-agent sessions are always hidden. nextCursor is opaque — round-trip it back to keep paging.
  • session/resume { sessionId, cwd, mcpServers } — adopts the session id without replaying. Use this when the client already has the history rendered. Same cwd-update behaviour as session/load.
  • session/close { sessionId } — cancels any in-flight prompt, releases the on-disk session lock, and removes the entry from the map.
  • session/cancel { sessionId } — interrupts the active session/prompt. The response carries stopReason: "cancelled". If a cancel arrives between turns (after one prompt completed and before the next is installed), meka latches the signal and cancels the next prompt immediately on arrival.
  • session/set_mode { sessionId, modeId } — flips the agent’s Permission cell. Modes outside [permissions].enabled from the config become JSON-RPC errors. On success, meka emits session/update: current_mode_update. The flip is atomic and applies to the next tool call within an in-flight turn — no need to wait for the turn to finish.

Prompt turn

A session/prompt carries a prompt array of ContentBlocks. meka accepts text and resource_link blocks (the ACP baseline). resource_link blocks are flattened into a <resource_link name="…" uri="…">description</resource_link> tag inside the prompt text so the model can see the reference; meka does not fetch the resource server-side. image, audio, and resource blocks are not yet supported and produce InvalidParams.

While the turn runs, meka streams session/update notifications:

  • agent_message_chunk for each piece of assistant text.
  • agent_thought_chunk for thinking blocks (Claude OAuth / extended-thinking models).
  • tool_call when a tool starts, with kind, status: "in_progress", an absolute locations array (relative paths resolved against the session cwd), and the raw input.
  • tool_call_update when a tool finishes, with the final status (completed / failed) and a content array. edit_file and write_file populate diff content blocks so clients can render the apply-diff UI.

The response carries a final stopReason:

stopReasonMeaning
end_turnThe agent finished cleanly.
max_tokensThe provider stopped because the model hit its maximum output tokens. The assistant message may be truncated.
cancelledsession/cancel interrupted the turn — including the case where the cancel caused an exception in an underlying operation. meka probes the per-session cancellation token after run_turn; any error returned while the token has fired surfaces as cancelled rather than a generic JSON-RPC error.
refusalThe model declined to comply (Claude stop_reason: "refusal" and the OpenAI equivalent). The assistant message contains the refusal text.

Permission modes

meka’s Permission levels map 1:1 to ACP SessionMode ids:

PermissionMode idDisplay nameDescription
NonenoneNoneNo tools available.
ReadreadReadFile reads and searches only. No writes, no shell.
AskaskAskEvery write or shell command requires approval.
WritewriteWriteAll tools allowed without per-call approval.

The full mode picker is advertised on every session-creation response (NewSessionResponse.modes, LoadSessionResponse.modes, ResumeSessionResponse.modes) but only the modes in [permissions].enabled from your config.toml are listed — picking a disabled mode would just error.

When the active mode is ask, write-gated tools trigger a session/request_permission round-trip. Clients render four options:

  • Allow — run this call only.
  • Always allow — run this call and skip the prompt for the same tool for the rest of the session.
  • Deny — refuse this call only.
  • Always deny — refuse this call and every subsequent call to the same tool.

Sticky decisions live in meka’s process memory; they reset on session close.

Skills as slash commands

Every installed skill (see Skills) is advertised through session/update: available_commands_update after session/new / session/load / session/resume, and refreshed at the top of every session/prompt so a skill installed mid-session shows up without a reconnect.

Each command carries a generic free-form input hint ("additional context (optional)"). When the user picks one from the palette, the client typically inserts /<skill-name> and lets the user type extra context. meka parses the prompt as follows:

  • Plain text (no leading slash) — passes through to the model unchanged.
  • /<skill-name> matching an installed skill — loads the skill body via the same path as the REPL’s /skill command (substituting ${MEKA_SKILL_DIR} and ${MEKA_SESSION_ID}) and prepends any extra context the user typed.
  • Slash with a syntactically valid but unknown skill name (/nonexistent) — JSON-RPC error.
  • Slash with content that isn’t a valid skill identifier (/etc/hosts, //comment) — passes through to the model unchanged, so pasted paths and code comments don’t get intercepted.

Sub-agents

spawn_agent and skill-based delegation produce a sub-agent that runs through PermissionForwardingFrontend. The sub-agent’s own output isn’t streamed to the client (its final report flows back through the parent’s tool_call_update), but its permission prompts, fs delegates, and terminal delegates all forward through the parent’s connection — so the editor’s apply-diff UI sees a sub-agent’s writes the same as the main agent’s.

Known limitations

  • MCP roots/list from background queries. During a tool call, roots/list reflects the calling session’s cwd via a task-local override. Outside of a tool call (e.g. server-initiated polling), the handler falls back to the process cwd, since the MCP protocol doesn’t carry session context.
  • Tool-call diff metadata isn’t persisted. A session reopened with session/load replays tool_call_updates as plain text rather than diffs. The on-disk content is unaffected.
  • read mode + terminal capability: meka runs the local sandboxed shell instead of delegating, to preserve the read-only jail. The shell appears in meka’s own output rather than the client’s terminal pane until you switch to ask or write.
  • Image and regex read_file: stay local. The fs/read_text_file request carries only text, so there’s no protocol surface to delegate either case.
  • Single content type in prompts: meka’s session/prompt accepts text only today. Image / audio / resource prompts will arrive as meka’s PromptCapabilities advertise them.

HTTP API

meka serve exposes meka as an HTTP API server so other programs can drive agent turns programmatically. Where Interactive Mode is for humans at a terminal and ACP is for editor integrations over stdio, the HTTP API is for service-to-service use cases:

  • A Telegram or Discord bridge that connects a chat bot to an agent.
  • A web or mobile UI that streams assistant responses in real time.
  • A script or orchestrator that embeds meka as a sub-agent backend.
  • Any cross-language client that speaks HTTP+JSON.

All three entry points (meka, meka acp, meka serve) drive the same agent core — same tools, same providers, same session persistence. The HTTP API is a transport layer on top.

Starting the server

meka serve

The server reads the [serve] section from your config.toml (see Configuration below). At minimum you need a bind address and at least one bearer token:

[serve]
bind = "127.0.0.1:8080"

[[serve.tokens]]
token = "${MEKA_API_TOKEN}"
scopes = ["sessions:r", "sessions:w"]

On startup the server logs the bind address and begins accepting requests. All endpoints (except health probes and OpenAPI docs) require a valid Authorization: Bearer <token> header.

TLS: meka serve speaks plain HTTP. For production, front it with a TLS-terminating reverse proxy (nginx, Caddy, Cloudflare Tunnel).

Quick example

Blocking turn (simplest)

# Create a session
curl -s -X POST http://localhost:8080/v1/sessions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"cwd": "/home/user/project"}' | jq .id
# → "550e8400-e29b-41d4-a716-446655440000"

# Submit a turn
curl -s -X POST http://localhost:8080/v1/sessions/550e8400-e29b-41d4-a716-446655440000/turn \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": "list the files in src/"}' | jq .final_text
# → "Here are the files in src/: ..."

Streaming turn

curl -N -X POST http://localhost:8080/v1/sessions/$SESSION_ID/turn \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": "explain this codebase", "stream": true}'

The response is a text/event-stream (SSE) that emits typed events as the agent works:

retry: 3000

event: turn.started
id: 0
data: {"turn_id":"...","session_id":"...","started_at":"2026-05-26T13:45:12Z"}

event: assistant_text.delta
id: 1
data: {"text":"This project is "}

event: assistant_text.delta
id: 2
data: {"text":"a Rust workspace that..."}

event: tool_call.executing
id: 3
data: {"id":"tu_1","name":"read_file","input":{"path":"src/main.rs"},"display_summary":"src/main.rs"}

event: tool_call.completed
id: 4
data: {"id":"tu_1","is_error":false,"content":[{"type":"text","text":"fn main() { ... }"}]}

event: turn.finished
id: 12
data: {"turn_id":"...","session_id":"...","stop_reason":"end_turn","usage":{"input_tokens":12340,"output_tokens":567,...}}

Core concepts

Sessions

A session is a persistent conversation with its own working directory, permission level, and message history. Sessions are stored in the same SQLite database as REPL and ACP sessions — they’re interchangeable.

POST   /v1/sessions          Create a session
GET    /v1/sessions           List sessions (paginated)
GET    /v1/sessions/{id}      Get session details
PATCH  /v1/sessions/{id}      Update permission or cwd
DELETE /v1/sessions/{id}      Close and clean up

When creating a session, specify the working directory and optionally a permission level and capabilities:

{
  "cwd": "/home/user/project",
  "permission": "write",
  "capabilities": {
    "supports_reasoning_stream": false
  }
}

The cwd field is validated on create and patch:

  • Must be an absolute path (no relative paths).
  • Must exist on the server’s filesystem.
  • Must be a directory (not a file, device, or socket).
  • Must not contain null bytes (which cause kernel/userspace path mismatch).

If cwd is omitted, it defaults to the server process’s current working directory.

Sessions persist server-side until explicitly deleted or evicted by the idle timeout GC (see Session lifecycle).

Turns

A turn is one round-trip: you send a user message, the agent processes it (potentially calling tools in a loop), and returns a result. Turns are ephemeral — they’re not stored as their own resource, but the messages they produce are persisted in the session’s conversation history.

POST   /v1/sessions/{id}/turn     Submit a turn
POST   /v1/sessions/{id}/cancel   Cancel an in-flight turn

One turn at a time per session. A second POST /turn while another is running returns 409 Conflict. Across sessions, turns run fully concurrently.

The turn request body accepts three fields:

FieldTypeDefaultDescription
messagestring(required)The user message
streamboolfalsefalse → single JSON response; true → SSE stream
options.skillstring | nullnullWhen set, activates the named skill for this turn (equivalent to /skill <name> in the REPL)

Messages

Read the conversation history for a session:

GET /v1/sessions/{id}/messages?offset=0&limit=50

Returns the full message list with role, content blocks, timestamps, and turn correlation IDs.

Blocking response

With stream: false (the default), the server holds the connection until the turn completes, then returns a single JSON response:

{
  "turn_id": "t_01J...",
  "session_id": "s_01J...",
  "stop_reason": "end_turn",
  "final_text": "Here are the files in src/: ...",
  "messages": [
    {
      "role": "assistant",
      "content": [{"type": "text", "text": "..."}]
    }
  ],
  "tool_calls": [
    {
      "id": "tu_1",
      "name": "read_file",
      "input": {"path": "src/main.rs"},
      "display_summary": "src/main.rs",
      "is_error": false,
      "content": [{"type": "text", "text": "..."}]
    }
  ],
  "usage": {
    "input_tokens": 12340,
    "output_tokens": 567,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 8000
  },
  "notices": []
}

Key fields:

  • final_text — concatenated assistant text. This is what most bots display to the user.
  • messages — structured message array for clients that want richer rendering.
  • tool_calls — every tool the agent called during the turn, with inputs and outputs.
  • stop_reasonend_turn, max_tokens, or refusal.
  • notices — provider advisories and auto-deny warnings.
  • refusal_text — present only when stop_reason is "refusal".

Streaming response

With stream: true, the response is a text/event-stream. Every event has a monotonic id, a named event type, and a JSON data payload.

Event types

Lifecycle

EventPayloadWhen
turn.startedturn_id, session_id, started_atTurn begins
turn.finishedstop_reason, usage, optional refusal_textTurn completed successfully
turn.failederror (Problem Detail shape)Turn failed mid-stream
turn.cancelledreason ("client" or "server_shutdown")Turn was cancelled

turn.finished, turn.failed, and turn.cancelled are terminal — the connection closes immediately after.

Content deltas

EventPayloadWhen
assistant_text.deltatextEach chunk of assistant text
thinking.deltatextExtended thinking content (only when supports_reasoning_stream: true)

Tool execution

EventPayloadWhen
tool_call.executingid, name, input, display_summaryTool call starts
tool_call.completedid, is_error, contentTool call finishes

Notices and pauses

EventPayloadWhen
noticelevel, textProvider advisories or warnings
permission_requiredrequest_id, tool_name, expires_in_secondsPermission approval needed (Ask mode)

Heartbeats

A : keep-alive comment is sent every 20 seconds. SSE clients ignore these automatically. The stream also sends retry: 3000 as its first line, hinting clients to reconnect after 3 seconds on disconnect.

SSE lag

The server buffers up to 256 events per SSE stream. If a consumer reads too slowly and falls behind, the server:

  1. Cancels the in-flight turn to stop burning provider tokens.
  2. Emits a terminal turn.failed event with error type https://meka.so/errors/sse-lag.
  3. Closes the stream.

The client should retry by submitting a new turn. Use GET /messages to inspect what the agent completed before the lag occurred.

Reconnection

There is no Last-Event-ID resumption. If the connection drops mid-turn, submit a new turn or use GET /messages to read what happened.

Permission modes over HTTP

The same four permission levels apply: none, read, ask, write. Set the level at session creation or update it via PATCH /v1/sessions/{id}.

Ask mode

In ask mode with stream: true, the agent emits a permission_required SSE event when it needs to run a gated tool. The stream stays open while waiting. Your client resolves it by POSTing to the responses endpoint:

POST /v1/sessions/{id}/responses/{request_id}
Content-Type: application/json

{"outcome": "allow"}

Possible outcomes:

OutcomeEffect
allowRun this tool call
denyRefuse this tool call
allow_alwaysAllow this and all future calls to this tool (session-scoped)
deny_alwaysDeny this and all future calls to this tool (session-scoped)

If no response arrives within 60 seconds, the permission defaults to deny.

Ask mode with blocking turns

When stream: false and the session is in ask mode, there is no SSE channel for permission prompts. The agent runs the turn with tool permissions auto-denied — each denied tool appends a notice to the response explaining what happened and suggesting permission: "write" or stream: true.

MCP elicitations (interactive form prompts from MCP servers) are always auto-declined over HTTP — there is no channel for interactive input. A notice event is emitted when this happens.

Recommendation: non-interactive callers (bots, bridges, scripts) should create sessions with permission: "read" or permission: "write" so auto-deny never triggers. Use stream: true if you need approval flow.

Authentication

Every request (except health probes and /v1/openapi.json) requires Authorization: Bearer <token>.

Scopes

Each token carries a set of scopes that control what it can access:

ScopePermits
sessions:rList sessions, get session details, read messages
sessions:wCreate, modify, delete sessions; submit and cancel turns; respond to permission prompts
skills:rList installed skills
mcp:rList MCP server status

Discovery endpoints (/v1/info, /v1/skills, /v1/mcp) accept any token with at least one read scope.

Token configuration

Tokens are configured under [[serve.tokens]] in your config. Three forms are supported:

# Inline plaintext — development only (a startup warning is logged)
[[serve.tokens]]
token = "sk_dev_test123"
scopes = ["sessions:r", "sessions:w"]

# Environment variable substitution — recommended for CI/containers
[[serve.tokens]]
token = "${MEKA_BRIDGE_TOKEN}"
description = "telegram bridge"
scopes = ["sessions:r", "sessions:w"]

# File-based — recommended for production (chmod 0600)
[[serve.tokens]]
token_file = "/etc/meka/bridge.token"
description = "telegram bridge"
scopes = ["sessions:r", "sessions:w"]

Token comparison uses constant-time equality to prevent timing side-channel attacks. Tokens never appear in logs — only a truncated SHA-256 fingerprint is used for diagnostics.

Idempotency

Blocking turn submissions (stream: false) support Stripe-style idempotency via the Idempotency-Key header:

curl -X POST http://localhost:8080/v1/sessions/$ID/turn \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f8a9b0c-1234-5678-abcd-ef0123456789" \
  -d '{"message": "deploy to staging"}'

If the same key is replayed, the server returns the cached response. If the same key is sent with a different body, it returns 409 Conflict. Keys are scoped per-token and expire after 24 hours.

Idempotency keys are ignored for streaming responses — streaming clients should reconnect by submitting a new turn.

Error handling

All HTTP error responses use RFC 9457 Problem Details with Content-Type: application/problem+json:

{
  "type": "https://meka.so/errors/session-not-found",
  "title": "Session not found",
  "status": 404,
  "detail": "Session 's_xyz' does not exist.",
  "instance": "/v1/sessions/s_xyz/turn"
}

The type URI is the stable, machine-readable error code. Route error handling on type, not on status or detail.

Error detail redaction: Validation errors (422) return a generic detail message (e.g. "invalid session creation request body") rather than echoing internal field names or parser diagnostics. Consult the OpenAPI spec for the expected request schema.

Error types

TypeStatusMeaning
/errors/auth401Missing or invalid bearer token
/errors/auth-scope403Token lacks the required scope
/errors/session-not-found404Unknown session ID
/errors/session-locked409Another meka process holds the session’s DB lock (e.g. two meka serve instances sharing one DB) — wait or restart the other process
/errors/turn-in-flight409A turn is already running on this session within this process — cancel it via POST /cancel first
/errors/turn-cancelled409Turn was cancelled
/errors/request-not-found404Unknown or expired request_id
/errors/idempotency409/429Key conflict (body mismatch: 409; cache cap: 429)
/errors/invalid-body422Request body validation failed
/errors/payload-too-large413Body exceeds max_body_bytes
/errors/concurrency-limit429Process-wide turn limit reached (Retry-After header included)
/errors/sse-lag500SSE consumer fell behind; stream terminated (see SSE lag)
/errors/provider502Upstream provider call failed
/errors/internal500Unhandled server error

Streaming turns that fail mid-stream emit a turn.failed SSE event with the same error shape, then close the connection.

Discovery endpoints

These endpoints help clients inspect the server’s capabilities at runtime.

EndpointAuthDescription
GET /v1/health/liveNoneLiveness probe — 200 if the process is up
GET /v1/health/readyNoneReadiness probe — 200 if provider, DB, and MCP servers are healthy. Returns status, session_db, provider_configured, and mcp_servers_healthy (boolean, no server names).
GET /v1/infoAny read scopeServer version, model, capabilities
GET /v1/skillsAny read scopeInstalled skills
GET /v1/mcpAny read scopeMCP server connection status
GET /v1/openapi.jsonNoneOpenAPI 3 spec
GET /v1/docsNoneSwagger UI

Session lifecycle

Idle timeout and GC

A background garbage collector scans in-memory sessions and evicts those that have been idle longer than idle_timeout:

[serve]
idle_timeout = "24h"
gc_scan_interval = "5m"

Eviction drops the in-memory state (agent runtime, conversation buffer, cancellation tokens) but keeps the SQLite row. A later request with the same session ID transparently re-attaches and continues the conversation.

To also remove the DB row on eviction:

[serve]
delete_on_idle = true

Sessions with an in-flight turn are never evicted.

Graceful shutdown

meka serve handles SIGTERM / SIGINT with a controlled drain:

  1. Stop accepting new connections.
  2. Cancel all in-flight turns (same mechanism as POST /cancel).
  3. Emit turn.cancelled with reason: "server_shutdown" on open SSE streams.
  4. Wait up to shutdown_drain_timeout for tasks to flush.
  5. Exit.
[serve]
shutdown_drain_timeout = "30s"

Concurrency

  • Per session: one turn at a time. A second POST /turn returns 409.
  • Across sessions: fully concurrent. Multiple sessions can run turns in parallel.
  • Process-wide cap (optional): set max_concurrent_turns to limit total in-flight turns. Exceeding the cap returns 429 with a Retry-After header.

Configuration

All settings live under [serve] in your config.toml. See the [serve] section of the config file reference for the full field list.

Minimal example:

[serve]
bind = "127.0.0.1:8080"

[[serve.tokens]]
token = "${MEKA_API_TOKEN}"
scopes = ["sessions:r", "sessions:w"]

Full example:

[serve]
bind = "0.0.0.0:8080"
max_body_bytes = 10485760           # 10 MiB (default)
max_concurrent_turns = 20
idle_timeout = "24h"
gc_scan_interval = "5m"
delete_on_idle = false
shutdown_drain_timeout = "30s"

# Bridge token — env var substitution
[[serve.tokens]]
token = "${BRIDGE_TOKEN}"
description = "telegram bridge"
scopes = ["sessions:r", "sessions:w"]

# Admin token — file-based
[[serve.tokens]]
token_file = "/etc/meka/admin.token"
description = "operator debugging"
scopes = ["sessions:r", "sessions:w", "mcp:r", "skills:r"]

Client recipes

Telegram bridge (Python)

import httpx

MEKA_URL = "http://localhost:8080"
MEKA_TOKEN = os.environ["MEKA_TOKEN"]

async def handle_message(chat_id: str, text: str):
    session_id = await get_or_create_session(chat_id)

    resp = await httpx.AsyncClient().post(
        f"{MEKA_URL}/v1/sessions/{session_id}/turn",
        headers={"Authorization": f"Bearer {MEKA_TOKEN}"},
        json={"message": text},
        timeout=httpx.Timeout(600.0, connect=5.0),
    )
    resp.raise_for_status()
    return resp.json()["final_text"]

Web UI (TypeScript, streaming)

const resp = await fetch(`${MEKA_URL}/v1/sessions/${sessionId}/turn`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ message: input, stream: true }),
});

const reader = resp.body!.getReader();
const decoder = new TextDecoder();
// ... parse SSE events from the stream

Shell script

#!/usr/bin/env bash
set -euo pipefail

TOKEN="sk_..."
BASE="http://localhost:8080"

# Create a session
SESSION=$(curl -sf -X POST "$BASE/v1/sessions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"cwd\": \"$(pwd)\"}" | jq -r .id)

# Run a turn
RESULT=$(curl -sf -X POST "$BASE/v1/sessions/$SESSION/turn" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": "summarize this project"}')

echo "$RESULT" | jq .final_text

# Clean up
curl -sf -X DELETE "$BASE/v1/sessions/$SESSION" \
  -H "Authorization: Bearer $TOKEN"

Reverse proxy setup

For production deployments behind nginx:

location /v1/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_buffering off;
    proxy_cache off;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_read_timeout 600s;
}

Key points:

  • Disable buffering — SSE events must not be buffered.
  • Extend read timeout — turns can take minutes; the default 60s is too short.
  • Do not compress — gzip/brotli on SSE responses swallow events. Exclude the /turn route from compression middleware.

Endpoint reference

MethodPathAuthDescription
GET/v1/health/liveLiveness probe
GET/v1/health/readyReadiness probe
GET/v1/inforeadServer info
GET/v1/skillsreadInstalled skills
GET/v1/mcpreadMCP server status
POST/v1/sessionssessions:wCreate session
GET/v1/sessionssessions:rList sessions
GET/v1/sessions/{id}sessions:rGet session
PATCH/v1/sessions/{id}sessions:wUpdate session
DELETE/v1/sessions/{id}sessions:wDelete session
GET/v1/sessions/{id}/messagessessions:rList messages
POST/v1/sessions/{id}/turnsessions:wSubmit turn
POST/v1/sessions/{id}/cancelsessions:wCancel turn
POST/v1/sessions/{id}/responses/{request_id}sessions:wResolve permission prompt
GET/v1/openapi.jsonOpenAPI spec
GET/v1/docsSwagger UI

For full request/response schemas, see /v1/openapi.json on a running server, or browse it interactively at /v1/docs (Swagger UI). Both endpoints are unauthenticated so CI pipelines and code generators can fetch the spec without a token.

Exporting the spec

Save a local copy for offline use or code generation:

curl -s http://localhost:8080/v1/openapi.json -o openapi.json

Code generation

Generate a typed client from the exported spec:

# Python (openapi-python-client)
openapi-python-client generate --path openapi.json

# TypeScript (openapi-typescript)
npx openapi-typescript openapi.json -o src/api.d.ts

# Go (oapi-codegen)
oapi-codegen -package api openapi.json > api/api.gen.go

# Rust (progenitor)
cargo progenitor-client openapi.json

Import into tools

  • Postman / Insomnia: Import → URL → http://localhost:8080/v1/openapi.json
  • Bruno: Create collection from OpenAPI → paste the URL or a saved file.
  • Swagger Editor: File → Import URL → http://localhost:8080/v1/openapi.json

Permissions

meka uses a four-level permission system to control what tools the agent can use. This gives you control over the agent’s capabilities and prevents accidental modifications.

Permission Levels

LevelIndicatorAllowed Tools
None[n] (green)No tools. The agent can only respond with text.
Read[r] (yellow)Read-only tools: read_file, find_files, search_contents, fetch_url, web_search, execute_command (sandboxed), todo_write, todo_read, spawn_agent, scratchpad tools
Ask[a] (magenta)All tools, but each call requires user approval (Y/n prompt)
Write[w] (red)All tools without restrictions: write_file, edit_file, execute_command (unsandboxed)

Each level includes all tools from the levels below it. Write mode includes all read tools.

Default Permission

The default permission is read. The default enabled set is none / read / writeask is opt-in: enable it under [permissions] in your config if you want approval prompts.

You can change the start mode with:

  • CLI flag: meka --permission write
  • Environment variable: export MEKA_PERMISSION=write
  • Config file: [permissions] default = "write" — see Config File

If --permission or MEKA_PERMISSION selects a mode that isn’t in [permissions].enabled, meka logs a warning and starts in the configured default instead of refusing to launch.

Changing Permissions at Runtime

Press Shift+Tab to cycle through permission levels:

none → read → ask → write → none → ...

Disabled modes are skipped during cycling. With the default enabled set, Shift+Tab cycles none → read → write → none.

Or use the /permission slash command:

/permission write
/permission ask

/permission <mode> against a disabled mode prints an error naming the currently enabled set.

The prompt indicator updates immediately to reflect the new level. The agent learns the current level via a per-turn [Permission context] block prepended to your message (see How Permissions Work below).

Ask Mode

In ask mode, the agent has access to all tools, but each tool call is paused for your approval:

[ask] Shell ls -la (Y/n)

Press Enter or y to approve, or n to deny. If denied, the agent receives an error and may try an alternative approach.

This mode is useful when you want the agent to have full capabilities but want to review each action before it executes.

How Permissions Work

When the agent attempts to use a tool, meka checks whether the current permission level allows it:

  • If allowed, the tool executes normally.
  • In ask mode, you are prompted to approve or deny.
  • If denied, meka returns an error message to the agent explaining which level is required and suggests running /permission <level>.

Telling the agent the current level

meka lists every registered tool in the system prompt with its required permission level inline — nothing is filtered out — and each user message carries a compact [Permission context] block:

<context>
[Permission context]
Current permission level: read
Only read-only tools are executable.
...
</context>

That two-line block is the only permission-dependent content in the request. The system prompt and the tools-array schemas stay byte-identical across /permission toggles, so mid-session level changes don’t invalidate the Claude prompt cache — the entire conversation stays warm.

MCP tool permissions

MCP tools are classified through a 5-step resolution chain: per-tool override → server-level override → the server’s own readOnlyHint[mcp].default_permission → hardcoded Write fallback. See the Permission resolution section of the Config File docs for the full rules and how to override a misclassified tool.

Built-in tool permissions

Any built-in tool’s required permission can be overridden from config.toml without editing code — see [tools] — built-in tool filters. The same section documents how to allow-list or block-list specific built-ins (e.g. disabling web_search in a locked-down environment).

Sub-agent permissions

Sub-agents spawned via spawn_agent inherit the parent’s permission level. In write mode the sub-agent can call write_file, edit_file, and unsandboxed execute_command; in read mode it’s confined to read-only tools. To run delegated work with reduced privileges, cycle the parent into read mode before issuing the spawning prompt.

Examples

Read Mode (Default)

meka [r] > read the contents of main.rs

The agent uses read_file and shows the contents. Shell commands also work in read mode, but run in a read-only sandbox – the filesystem is physically write-protected for the child process:

meka [r] > list the files in this directory
meka [r] > show me the git log

Commands like ls, cat, git log, df, ps, and uname work normally. Commands that attempt to write to the filesystem (e.g., touch, rm, mkdir) will fail with a permission error.

If you ask the agent to modify a file:

meka [r] > add a comment to the top of main.rs

The agent will explain that it cannot write files in read mode and suggest switching to write mode.

Note: The read-only sandbox uses Landlock on Linux (kernel 5.13+) and sandbox-exec on macOS. On platforms where sandboxing is unavailable, shell commands are not available in read mode. You can disable sandboxed shell execution by setting sandbox = false under [shell] in the config file (see Config File).

Write Mode

meka [w] > run cargo test and show me the output

The agent uses execute_command to run the tests and shows the results.

Sessions

Sessions persist your conversation so you can resume later. Each session is identified by a UUID and stored in a SQLite database.

How Sessions Work

  • A session is not created when meka starts. It is created lazily when you send the first message.
  • When a session is created, its UUID is printed to stderr.
  • When you exit meka (Ctrl+D), the session UUID is printed again so you can note it for later.
  • Sessions include the full conversation: your inputs, the agent’s responses, and tool call results.

Resuming a Session

Continue Last Session

meka -c

This resumes the most recently updated session.

By UUID

meka -c 550e8400-e29b-41d4-a716-446655440000

The agent loads the previous conversation and continues from where you left off.

By UUID Prefix

If the value isn’t a valid UUID, meka treats it as a leading prefix and looks up sessions whose ID starts with it. This avoids having to copy the entire UUID:

meka -c 550e            # works if exactly one session starts with `550e`
meka -c 5               # likely ambiguous; meka lists matching IDs and exits

When a prefix matches multiple sessions, meka prints the matching IDs (most-recent first) so you can disambiguate. Type a few more characters until the prefix is unique.

Session Locking

Only one meka instance can be attached to a session at a time. This prevents race conditions from concurrent writes.

  • If you try to resume a session that is locked by a running meka process, you will get an error.
  • If the locking process has exited (crashed or was killed), meka detects this and allows you to take over the lock.

Storage Location

Sessions are stored in a SQLite database at a platform-specific location:

PlatformPath
Linux~/.local/share/meka/sessions.db ($XDG_DATA_HOME/meka/sessions.db)
macOS~/Library/Application Support/meka/sessions.db
Windows%APPDATA%\meka\sessions.db

Database Schema

The database has three tables:

sessions – one row per session:

ColumnTypeDescription
idTEXT (UUID)Primary key
created_atTEXT (RFC 3339)When the session was created
updated_atTEXT (RFC 3339)When the session was last updated
locked_byTEXT (PID)PID of the process holding the lock, or NULL
metadataTEXTReserved for future use

messages – one row per message in a session:

ColumnTypeDescription
idINTEGERAuto-incrementing primary key
session_idTEXT (UUID)Foreign key to sessions.id
roleTEXTuser, assistant, or tool_results
contentTEXTMessage content (plain text or JSON)
created_atTEXT (RFC 3339)When the message was saved

tool_outputs – scratchpad entries, one row per entry:

ColumnTypeDescription
session_idTEXT (UUID)Part of composite primary key
nameTEXTPart of composite primary key
contentTEXTThe stored content
created_atTEXT (RFC 3339)When the entry was created

Scratchpad entries are scoped to a session. Two sessions can have entries with the same name. Entries are preserved across compaction but deleted when a session is deleted.

History Retention

meka automatically manages session storage on startup with sensible defaults:

  • retention_days (default: 90) – deletes sessions whose updated_at is older than this many days.
  • max_storage_bytes (default: 52428800 / 50 MB) – when total message content exceeds this limit, the oldest sessions are deleted until the total is under the limit.

You can override these defaults in the config file under [session]:

[session]
retention_days = 30          # delete sessions not used in 30 days
max_storage_bytes = 10485760 # cap total storage at ~10 MB

See Config File for details.

Context Window Limiting

Long sessions can exceed the LLM’s context window or become expensive. The context_messages setting (default: 200) limits how many recent messages are sent to the API:

[session]
context_messages = 100

The full history remains in SQLite for resumption. Only the API payload is truncated. The truncation preserves tool call chains (it never splits a tool use from its result).

Compacting a Session

If a session becomes too long, you can use the /compact command to have the LLM summarize the conversation and replace older messages with a structured summary. Recent messages are preserved verbatim. The summary includes key files, decisions, errors, and user preferences.

Compaction preserves scratchpad entries and the todo list, and re-injects environment context so the agent isn’t disoriented after compaction. Tools that the model loaded via load_tool before compaction stay loaded after — the deferred-tool active set is snapshotted into the compaction boundary, so resumed sessions don’t re-issue load_tool for tools they already used.

Internally, compaction does not delete pre-compaction rows from the database. It appends a compact_boundary row to the messages table; the materialized view is reconstructed from the event log, so the persisted log itself stays append-only.

Auto-Compact

When auto_compact is enabled (default: true), meka automatically compacts the conversation when the input token count exceeds 80% of the context window. This runs between turns, not during tool loops.

[session]
auto_compact = true
context_window = 200000  # optional override

Listing Sessions

To see past sessions:

meka list

This shows a table with each session’s ID, last update time, and a preview of the first message:

ID                                    Updated              Preview
550e8400-e29b-41d4-a716-446655440000  2026-03-14 12:00:00  How do I implement a binary search tree?
a1b2c3d4-e5f6-7890-abcd-ef1234567890  2026-03-13 09:30:00  Fix the login page CSS

By default the 20 most recent sessions are shown. Use -n to change:

meka list -n 50

Exporting a Session

You can export any session as a Markdown file:

meka export 550e8400-e29b-41d4-a716-446655440000

This writes session-550e8400-e29b-41d4-a716-446655440000.md in the current directory with the full conversation. User and assistant messages are rendered as Markdown sections, while tool calls and results are wrapped in collapsible <details> blocks.

To write to a specific file:

meka export 550e8400-e29b-41d4-a716-446655440000 -o conversation.md

To print to stdout (for piping):

meka export 550e8400-e29b-41d4-a716-446655440000 -o -

Deleting Sessions

Delete specific sessions by UUID:

meka delete 550e8400-e29b-41d4-a716-446655440000

Delete multiple sessions at once:

meka delete 550e8400-e29b-41d4-a716-446655440000 a1b2c3d4-e5f6-7890-abcd-ef1234567890

Delete all sessions:

meka delete --all

Managing Sessions via SQLite

You can also manage sessions directly through the SQLite database. For example, to list all sessions:

sqlite3 ~/.local/share/meka/sessions.db \
  "SELECT id, created_at, updated_at FROM sessions ORDER BY updated_at DESC;"

Skills

Skills are user-defined knowledge packages that give the agent non-standard knowledge – manuals, procedures, tool-specific instructions, and experience the LLM doesn’t have natively. Each skill is a directory containing a SKILL.md file with structured metadata.

How Skills Work

  • Skills live in ~/.config/meka/skills/ (platform-specific config dir).
  • Each skill is a directory: skills/<name>/SKILL.md.
  • Any entry whose name begins with . is skipped at discovery. This covers VCS metadata (.git), editor/IDE state (.vscode, .idea), filesystem artifacts (.DS_Store, .Trash), and any other dotfile or dotdir that may sit alongside your skills.
  • SKILL.md starts with a YAML frontmatter block declaring the skill’s metadata, followed by Markdown body content.
  • On every prompt, meka discovers all valid skills and lists them in the system prompt with their description.
  • The agent invokes a skill by calling the skill tool with the skill name. The tool returns the full body, which the agent follows.
  • Skills are available in read, ask, and write permission modes (not in none).

File Format

A skill is a directory under ~/.config/meka/skills/ containing a SKILL.md file:

~/.config/meka/skills/
└── download-videos/
    └── SKILL.md

SKILL.md must begin with a YAML frontmatter block, followed by the skill body:

---
description: Download videos from various websites using yt-dlp. Use when the user wants a video off a URL.
version: "1.0"
author: John Doe <john.doe@example.com>
source_url: https://gist.githubusercontent.com/k4yt3x/.../raw/SKILL.md
---

# Download Videos with yt-dlp

## Installation

Install yt-dlp:

\```bash
pip install yt-dlp
\```

## Basic Usage

Download a video:

\```bash
yt-dlp "https://example.com/video"
\```

Required Frontmatter Fields

FieldDescription
descriptionSummary of what the skill does and when to invoke it. Shown in the system prompt — fold the trigger condition into this one line.

Skills missing description are skipped at discovery with a warning log. Unknown frontmatter keys are ignored, so a skill authored for Claude Code (which carries extra keys like when_to_use or allowed-tools) still loads.

Optional Frontmatter Fields

FieldDefaultDescription
versionnoneFree-form version label (e.g. "1.0", "2024-03-14").
authornoneAttribution, conventionally Name <email> (e.g. John Doe <john.doe@example.com>). Informational only.
source_urlnoneAn https:// URL the skill’s SKILL.md can be re-fetched from. Enables meka skill update.

Variable Substitution

The skill body may reference these variables, which are expanded when the skill is loaded:

  • ${MEKA_SKILL_DIR} – the absolute path to the skill’s directory. Use this to reference bundled helper files (e.g. ${MEKA_SKILL_DIR}/helper.sh).
  • ${MEKA_SESSION_ID} – the current session UUID.

Storage Location

PlatformPath
Linux~/.config/meka/skills/<name>/SKILL.md ($XDG_CONFIG_HOME/meka/skills/)
macOS~/Library/Application Support/meka/skills/<name>/SKILL.md
Windows%APPDATA%\meka\skills\<name>\SKILL.md

How the Agent Uses Skills

When skills are available, the system prompt includes a ## Skills section like:

## Skills

- **download-videos**: Download videos from various websites using yt-dlp. Use when the user wants a video off a URL.
- **deploy-kubernetes**: Deploy services to a K8s cluster. Use when the user asks to deploy to Kubernetes.

The agent loads a skill by calling the skill tool:

skill(name: "download-videos")

The tool returns the full body of SKILL.md (with variables expanded) as its output. The agent then follows the instructions.

Whenever a skill body is loaded — by the skill tool, --skill, /skill, spawn_agent, or meka skill show — it is prefixed with a one-line header naming the skill’s directory:

Base directory for this skill and its bundled files: /home/user/.config/meka/skills/download-videos

This lets the agent locate files bundled alongside SKILL.md even when the body refers to them by bare name (e.g. helper.sh) rather than via ${MEKA_SKILL_DIR}.

Running a Skill in a Sub-Agent

The agent can delegate a skill to a sub-agent by passing the skill parameter to the spawn_agent tool. The sub-agent runs the skill in its own fresh context and returns a report, keeping the skill’s instructions out of the parent’s conversation:

spawn_agent(skill: "summarize-financial-news")
spawn_agent(skill: "summarize-financial-news", prompt: "focus on UK markets")

prompt is optional when skill is given; if both are supplied, prompt is prepended to the skill body as extra direction — the same ordering as meka --skill <name> [prompt].

Invoking a Skill from the CLI

Any skill can be triggered directly from the command line with --skill <name>. The rendered body becomes the first user turn, and meka drops into the interactive REPL after the turn finishes:

meka --skill download-videos "https://example.com/video"

The positional [PROMPT] argument, if given, is prepended to the skill body as extra context — equivalent to typing /skill download-videos https://example.com/video in the REPL.

To run the skill and exit immediately (useful for scripts), pair with --oneshot:

meka --oneshot --skill download-videos "https://example.com/video"

To invoke a skill mid-session inside the REPL, use the slash command instead:

/skill download-videos
/skill download-videos this URL specifically

Updating Skills

A skill that declares a source_url can be re-fetched and replaced on disk with meka skill update:

meka skill update download-videos   # update one skill
meka skill update --all             # dry run: lists what would update
meka skill update --all --yes       # apply the updates

source_url should be an https:// link to a raw SKILL.md (e.g. a GitHub raw URL or a gist raw URL). The fetch is validated — the response must parse as a valid skill — before the on-disk file is atomically replaced, so a 404 page or a malformed file leaves the existing skill untouched. If the fetched content is byte-identical to what’s on disk, nothing is written.

meka skill update --all without --yes is a dry run: it lists every skill that would be updated and applies nothing. This is the confirmation gate for a bulk remote fetch — re-run with --yes to apply.

Only the SKILL.md file is fetched. Helper scripts bundled alongside it in the skill directory are not updated this way — source_url-based update is for single-file skills.

Trust note. A skill body is instructions the agent follows. meka skill update replaces that content with whatever the source_url currently serves — review the source you point it at, and prefer --all (with its dry-run default) over blind updates.

Tips

  • Use short, unambiguous skill names (e.g. setup-postgres, not pg). The name is what the agent sees and calls.
  • Write description concisely, and fold the “use when…” trigger into it – it goes into every system prompt and consumes tokens.
  • Keep each skill focused on a single topic or procedure. Spawn multiple skills rather than one giant one.
  • Bundle supporting files in the skill directory and reference them with ${MEKA_SKILL_DIR}/file.ext.
  • Skills are re-discovered on every prompt, so you can add, edit, or remove skills mid-session without restarting meka.

Providers Overview

Providers are the LLM inference backends that meka uses to process your instructions. meka ships with four built-in providers:

ProviderAuthAPINotes
openai-apiOPENAI_API_KEYChat CompletionsWorks with OpenAI and any compatible endpoint (Ollama, vLLM, OpenRouter, …)
openai-codexOAuth login (setup wizard)OpenAI ResponsesUses a ChatGPT subscription; talks to chatgpt.com/backend-api/codex like the Codex CLI
claude-apiCLAUDE_API_KEYClaude MessagesDirect Claude API, billed per-token
claude-oauthOAuth login (setup wizard)Claude MessagesUses a Claude Code subscription; replicates Claude Code’s request shape and attestation

Selecting a Provider

Set the provider via any configuration layer:

# CLI flag
meka --provider claude-oauth

# Environment variable
export MEKA_PROVIDER=claude-api

# Config file (~/.config/meka/config.toml)
[provider]
name = "openai-api"

OpenAI-Compatible APIs

The openai-api provider works with any API that implements the OpenAI Chat Completions format. This includes:

  • OpenAI (default endpoint)
  • Ollama (http://localhost:11434/v1)
  • OpenRouter (https://openrouter.ai/api/v1)
  • vLLM, LiteLLM, and other OpenAI-compatible servers

Set the --base-url flag or OPENAI_BASE_URL environment variable to point at the alternative endpoint.

claude-api vs claude-oauth

Both talk to Claude’s /v1/messages endpoint, but the auth and request shape differ:

  • claude-api is the straightforward path — an x-api-key header, a plain system prompt, no extra headers. Choose this when you have a Claude API key.
  • claude-oauth replicates the Claude Code CLI exactly: OAuth tokens, fingerprint-encoded version header, xxHash64 attestation over the request body, injected billing system block. Choose this when you want to use a Claude Code subscription. Any deviation from the expected shape causes requests to be rejected, so avoid proxies that rewrite headers or reformat the body.

openai-api vs openai-codex

The two OpenAI-flavoured providers hit different endpoints with different protocols:

  • openai-api posts to /chat/completions on api.openai.com (or any compatible endpoint), authenticating with an API key. This is the right choice when you have an OpenAI billing account or are pointing at a self-hosted OpenAI-compatible server.
  • openai-codex posts to chatgpt.com/backend-api/codex/responses using the OpenAI Responses API (a different protocol — different request body shape, different streaming events). Authentication is OAuth against auth.openai.com, mirroring the first-party Codex CLI. Choose this to use a ChatGPT Plus / Pro / Team / Business subscription instead of a per-token API key.

Streaming vs Non-Streaming

By default, meka uses streaming mode: tokens appear in the terminal as they are generated. Use --no-stream to wait for the complete response before displaying it.

Streaming is recommended for interactive use. Non-streaming may be useful for scripting or when the provider does not support SSE.

OpenAI API Provider

The openai-api provider uses the Chat Completions API. It also works with any OpenAI-compatible API endpoint (Ollama, vLLM, OpenRouter, etc.).

Configuration

SettingValue
Provider nameopenai-api
Default base URLhttps://api.openai.com/v1
API key env varOPENAI_API_KEY
Auth methodBearer token (Authorization: Bearer <key>)

Minimal Setup

export MEKA_PROVIDER=openai-api
export MEKA_MODEL=gpt-4o
export OPENAI_API_KEY=sk-...
meka

Config File

[provider]
name = "openai-api"
model = "gpt-4o"

Supported Models

Any model available through the OpenAI Chat Completions API (or compatible endpoint) that supports tool calling:

  • gpt-4o, gpt-4o-mini
  • gpt-4-turbo
  • o1, o3-mini
  • Third-party models via compatible APIs

Custom Base URL

To use an OpenAI-compatible endpoint, set the base URL:

# Ollama
meka --provider openai-api --model llama3 --base-url http://localhost:11434/v1

# OpenRouter
meka --provider openai-api --model anthropic/claude-sonnet-4-20250514 --base-url https://openrouter.ai/api/v1

Or in the config file:

[provider]
name = "openai-api"
model = "llama3"
api_key = "unused"
base_url = "http://localhost:11434/v1"

API Details

Endpoint: POST {base_url}/chat/completions

Tool format: Tools are sent as function definitions:

{
  "type": "function",
  "function": {
    "name": "read_file",
    "description": "Read the contents of a file at the given path.",
    "parameters": { "type": "object", "properties": { ... } }
  }
}

Tool results: Sent back as messages with role: "tool" and the corresponding tool_call_id.

Streaming: Uses Server-Sent Events (SSE) with data: {...} lines. The stream ends with data: [DONE].

OpenAI Codex Provider

The openai-codex provider talks to OpenAI’s subscription endpoint using the OAuth tokens issued by ChatGPT login. It’s the OpenAI counterpart to claude-oauth: instead of paying per-token via an API key, you authenticate with your ChatGPT Plus / Pro / Team / Business / Enterprise account and your usage counts against your subscription.

Note: This provider replicates the wire shape that OpenAI’s first-party Codex CLI sends. It targets chatgpt.com/backend-api/codex/responses using the OpenAI Responses API — a different protocol than openai-api, which uses Chat Completions against api.openai.com. The two providers are not interchangeable.

Configuration

SettingValue
Provider nameopenai-codex
Default base URLhttps://chatgpt.com (request path /backend-api/codex/responses)
Auth methodOAuth 2.0 Authorization Code with PKCE
OAuth issuerhttps://auth.openai.com
Required tierChatGPT Plus, Pro, Team, Business, Enterprise, or Edu

Initial Setup

meka setup
# Pick "openai-codex (ChatGPT subscription login)"
# A browser opens; sign in to ChatGPT and approve.
# Tokens are saved to ~/.local/share/meka/sessions.db (chmod 0600).

The wizard binds a local listener on 127.0.0.1:1455 to receive the OAuth callback, matching the redirect URI registered with OpenAI’s auth server. If port 1455 is already in use (e.g. you’re already running the Codex CLI), free it first.

Config File

[provider]
name = "openai-codex"
model = "gpt-5"
effort = "high"   # optional; "low" | "medium" | "high"

The effort field maps to the Responses API reasoning.effort knob and is only consumed by reasoning-capable models (gpt-5, o-series). It defaults to "high".

Supported Models

Whatever your ChatGPT subscription tier exposes — typically gpt-5, gpt-5-codex, o3, o4-mini, etc. The model field on the request body is forwarded verbatim; meka doesn’t gate which model strings are valid.

How It Works

Each request:

  1. Auth header set: Authorization: Bearer <access_token>, ChatGPT-Account-ID: <workspace_id> (extracted from the JWT id_token at login), originator: meka_cli, plus a User-Agent identifying meka.
  2. Cookie jar enabled: chatgpt.com is fronted by Cloudflare; bot-clearance cookies (__cf_bm etc.) persist across requests automatically.
  3. Body: standard Responses API JSON — instructions, input (an array of message / function_call / function_call_output items), tools, optional reasoning.effort.
  4. Stream: SSE events: response.output_text.delta for text, response.output_item.added / …done for tool calls, response.reasoning_text.delta for thinking, response.completed for end-of-turn with token usage.
  5. Token refresh: when the access token is within 5 minutes of expiry, meka transparently refreshes it against auth.openai.com/oauth/token before the next request.

Limitations

  • Streaming-only: the Codex endpoint doesn’t support non-streaming completions. meka always streams for this provider; --no-stream is rejected with an explicit error.
  • Subscription required: you need a paid ChatGPT plan with Codex enabled. Free-tier accounts can complete the OAuth flow but most models will reject requests at the API layer.
  • Bot detection: chatgpt.com may serve a Cloudflare challenge if request patterns look automated. meka’s reqwest client handles cookie-clearance automatically; if you hit a hard challenge, complete it once in a regular browser to refresh the cookies.
  • Endpoint stability: this is OpenAI’s subscription-internal API; OpenAI doesn’t guarantee compatibility for third-party clients. Future Codex versions could add request signing or rotate scopes; meka will need updates if that happens.

Subscription vs API Key

If you have both a ChatGPT subscription and an OpenAI API key:

  • Use openai-codex for interactive work — it’s billed against your subscription’s usage cap rather than per-token, so heavy use is cheaper for most personal patterns.
  • Use openai-api for scripted / unattended work — API keys are stable, work with non-OpenAI Chat-Completions-compatible servers (Ollama, vLLM, OpenRouter), and don’t depend on the Cloudflare cookie jar.

Logging Out

Removing the saved tokens:

sqlite3 ~/.local/share/meka/sessions.db "DELETE FROM oauth_tokens WHERE provider = 'openai-codex'"

Then re-run meka setup to log in again with a fresh PKCE pair.

Claude API Provider

The claude-api provider talks to the Claude Messages API directly with an x-api-key header. Use this when you have a Claude API key (billed per-token). For Claude Code OAuth, see claude-oauth.

Configuration

SettingValue
Provider nameclaude-api
Default base URLhttps://api.anthropic.com
API key env varCLAUDE_API_KEY
Auth methodx-api-key header
API version2023-06-01

Minimal Setup

export MEKA_PROVIDER=claude-api
export MEKA_MODEL=claude-opus-4-6
export CLAUDE_API_KEY=sk-ant-api03-...
meka

Config File

[provider]
name = "claude-api"
model = "claude-opus-4-6"

Supported Models

Any model available through the Claude Messages API. Current line-up (per Anthropic’s models overview):

FamilyAliasNotes
Opus 4.7claude-opus-4-7Latest Opus — most capable, adaptive thinking
Sonnet 4.6claude-sonnet-4-6Latest Sonnet — speed + intelligence balance
Haiku 4.5claude-haiku-4-5Latest Haiku — fastest

Older but still available: claude-opus-4-6, claude-sonnet-4-5, claude-opus-4-5, claude-opus-4-1. Deprecated and retiring 2026-06-15: claude-opus-4-20250514, claude-sonnet-4-20250514.

Custom Base URL

To use a Claude-API-compatible proxy or gateway:

meka --provider claude-api --model claude-opus-4-6 --base-url https://my-proxy.example.com

API Details

Endpoint: POST {base_url}/v1/messages

Headers:

  • x-api-key: <api_key>
  • anthropic-version: 2023-06-01
  • content-type: application/json

System prompt: Sent as a top-level system string.

Tool format: Tools are defined with input_schema:

{
  "name": "read_file",
  "description": "Read the contents of a file at the given path.",
  "input_schema": { "type": "object", "properties": { ... } }
}

Streaming: Server-Sent Events with named event types (message_start, content_block_start, content_block_delta, content_block_stop, message_delta, message_stop, ping).

Claude OAuth Provider

The claude-oauth provider authenticates with a Claude Code subscription via OAuth and mimics the Claude Code CLI’s exact request shape, headers, and request signing. Use this when you have a Claude Code subscription instead of a per-token Claude API key. For the direct Messages API, see claude-api.

Note: This provider replicates Claude Code’s fingerprinting and attestation machinery exactly. Modifying the request body, headers, or OAuth flow will cause requests to be rejected by Anthropic. If you hit 401/403 errors, verify that no middleware is rewriting the request.

Configuration

SettingValue
Provider nameclaude-oauth
Default base URLhttps://api.anthropic.com
OAuth token env varCLAUDE_OAUTH_TOKEN
OAuth client ID env varCLAUDE_CLIENT_ID (optional)
Auth methodAuthorization: Bearer <oauth_token>
API version2023-06-01

Quickest Start (Setup Wizard)

meka setup

Pick claude-oauth when prompted. The wizard opens your browser, walks you through authorization, and saves the tokens to the local database.

Minimal Setup (Manual OAuth Token)

export MEKA_PROVIDER=claude-oauth
export MEKA_MODEL=claude-opus-4-6
export CLAUDE_OAUTH_TOKEN=sk-ant-oat01-...
meka

On first run the OAuth token is saved to the database. Subsequent runs load it automatically; you no longer need the env var.

Config File

[provider]
name = "claude-oauth"
model = "claude-opus-4-6"
effort = "high"          # optional; "low" | "medium" | "high"
redact_thinking = false  # optional; redact thinking content for privacy
# device_id, oauth_token_url, oauth_token are all optional overrides

See Configuration → Config File for the full list of fields.

Provider-specific knobs

[provider].effort

Sent as output_config.effort for effort-capable models (opus-4-6, sonnet-4-6). Accepts "low", "medium", or "high". Defaults to "high". Mirrors Claude Code’s effort knob in utils/effort.ts. Older models (Sonnet 4.0, Opus 4.1, Haiku 4.5) ignore this field on the wire and the body field is omitted automatically.

[provider].redact_thinking

When true, meka adds the redact-thinking-2026-02-12 beta header so the server returns redacted thinking blocks instead of full thinking summaries. The redacted payloads can’t be replayed back to the server in multi-turn conversations, so meka stores them as opaque signatures only. Defaults to false.

[provider].device_id

Stable per-machine identifier embedded in metadata.user_id to mirror Claude Code’s ~/.claude.json device ID (getOrCreateUserID in utils/config.ts).

If unset, meka first tries to adopt userID from ~/.claude.json (so meka and Claude Code on the same machine present as the same device). If that file is missing or has no userID, meka generates a 64-character hex string. Either way the resolved value is persisted back to [provider].device_id in config.toml. Other providers ignore this field — no stub config file is written for them.

Authentication

meka setup performs an OAuth 2.0 Authorization Code flow with PKCE:

  1. meka generates a PKCE challenge and opens your browser to Claude’s authorization page.
  2. You authorize the application in your browser.
  3. You paste the authorization code back into meka (the redirect URI is the platform.claude.com hosted callback page, not a local listener).
  4. meka exchanges the code for access + refresh tokens.
  5. Tokens are stored in the local database and refreshed automatically.

The OAuth client ID defaults to Claude Code’s client ID but can be overridden via the CLAUDE_CLIENT_ID env var.

Token Lifecycle

  1. Provide the initial token via setup wizard, env var, or config.
  2. meka saves it to the database on first use.
  3. On subsequent launches the token is loaded from the database.
  4. meka refreshes the access token automatically when it’s within 5 minutes of expiry; the new token is written back to the database.
  5. Setting a new env var or config value replaces the stored token.

Token refresh URL: defaults to https://api.anthropic.com/v1/oauth/token. Configurable via provider.oauth_token_url in the config file.

Supported Models

Any model your Claude Code subscription exposes. Current line-up (per Anthropic’s models overview):

FamilyAliasNotes
Opus 4.7claude-opus-4-7Latest Opus — most capable, no extended-thinking, adaptive thinking
Sonnet 4.6claude-sonnet-4-6Latest Sonnet — speed + intelligence balance
Haiku 4.5claude-haiku-4-5Latest Haiku — fastest

Older but still available: claude-opus-4-6, claude-sonnet-4-5, claude-opus-4-5, claude-opus-4-1. Deprecated and retiring 2026-06-15: claude-opus-4-20250514, claude-sonnet-4-20250514.

meka forwards the model string verbatim — it doesn’t gate which strings are valid. Per-model behaviour depends on capability gates baked into the request shape (see Beta header). The current gates target opus-4-6 / sonnet-4-6 for adaptive-thinking and effort; newer models (e.g. Opus 4.7) fall through to the conservative defaults until the gates are updated.

API Details

Endpoint: POST {base_url}/v1/messages?beta=true

Authentication & identity headers:

  • Authorization: Bearer <oauth_token>
  • anthropic-version: 2023-06-01
  • anthropic-beta: <comma-separated beta list> (computed per request, see below)
  • x-app: cli
  • User-Agent: claude-cli/<version> (external, cli)
  • X-Claude-Code-Session-Id: <uuid> (per-process)
  • Stainless SDK identification headers (x-stainless-*)

Beta header

Composed dynamically from the model + thinking settings, mirroring Claude Code’s getAllModelBetas (utils/betas.ts). Order is significant — wire dumps from Claude Code show this exact ordering:

BetaWhen
claude-code-20250219All models except Haiku family
oauth-2025-04-20Always (subscription auth)
adaptive-thinking-2026-01-28Thinking on AND model is opus-4-6 / sonnet-4-6
interleaved-thinking-2025-05-14Thinking on AND model is older Claude 4 (Sonnet 4.0, etc.)
redact-thinking-2026-02-12[provider].redact_thinking = true AND thinking on
context-management-2025-06-27Any modern Claude (4.x+)
prompt-caching-scope-2026-01-05Always
effort-2025-11-24opus-4-6 / sonnet-4-6 only

System prompt

Sent as an array of three text blocks:

  1. x-anthropic-billing-header: cc_version=<version>.<fingerprint>; cc_entrypoint=cli; cch=<xxHash64-attestation>; — the fingerprint suffix is a 3-character hex hash derived from the first user message (SHA256(salt + msg[4] + msg[7] + msg[20] + version)[:3]); the cch token is xxHash64 of the entire serialized request body, computed and patched in just before send.
  2. You are Claude Code, Anthropic's official CLI for Claude. — fixed identity prefix.
  3. Your own system prompt — carries cache_control: {type: "ephemeral", ttl: "1h"}.

Only block 3 is marked for caching, matching the recent Claude Code wire shape (“boundary mode” in utils/api.ts:362-409). Blocks 1 and 2 must come first so the cch=00000 placeholder is the first occurrence in the serialized JSON, which is what patch_request_body looks for when computing the attestation.

Other body fields

  • metadata.user_id: JSON-encoded {"device_id": "...", "account_uuid": "", "session_id": "..."}device_id from [provider].device_id, session_id is per-process.
  • context_management.edits = [{type: "clear_thinking_20251015", keep: "all"}] — present when thinking is enabled on a context-management-capable model. Mirrors Claude Code’s apiMicrocompact.
  • output_config.effort: present for effort-capable models, value from [provider].effort.
  • temperature: 1 — only when thinking is disabled.
  • max_tokens64_000 for adaptive-thinking models, max(thinking_budget * 2, 32_000) for legacy thinking models, 32_000 otherwise.

Cache control

The most recent message’s last content block, the last tool definition, and the user system prompt all carry cache_control: {type: "ephemeral", ttl: "1h"}. The 1h TTL matches Claude Code’s getCacheControl for OAuth subscribers (should1hCacheTTL in claude.ts:358-374). Mid-session permission toggles never invalidate this cache — see Permissions for the reasoning.

Streaming

Server-Sent Events with the same event taxonomy as claude-api: content_block_start, content_block_delta, content_block_stop, message_delta, message_stop. Reasoning streams as thinking_delta events; redacted thinking arrives as a single [redacted] block plus a signature.

Tools Overview

Tools are the actions that the agent can perform on your behalf. The LLM decides which tools to call based on your instructions.

Available Tools

ToolPermissionDescription
read_fileReadRead file contents
edit_fileWriteMake string replacements in a file
write_fileWriteCreate or overwrite a file
find_filesReadFind files by glob pattern
search_contentsReadSearch file contents with regex
fetch_urlReadFetch a web page as markdown
web_searchReadSearch the web
execute_commandRead/WriteRun a shell command
todo_writeReadManage a structured task list
todo_readReadRead the current task list
spawn_agentReadDelegate tasks to a sub-agent
scratchpad_writeReadStore content in the scratchpad
scratchpad_readReadRead a scratchpad entry
scratchpad_editReadEdit a scratchpad entry
scratchpad_listReadList scratchpad entries
scratchpad_deleteReadDelete a scratchpad entry
skillReadLoad a named skill’s instructions
render_imageReadView an image from in-memory base64 or scratchpad

Permission Requirements

Tools are grouped by the minimum permission level required:

Read permission (available in read, ask, and write modes):

  • read_file, find_files, search_contents, fetch_url, web_search
  • execute_command (sandboxed, filesystem write-protected)
  • todo_write, todo_read, spawn_agent, skill, render_image
  • All scratchpad tools

Write permission (only available in write mode):

  • edit_file, write_file, execute_command (unsandboxed)

In ask mode, all tools are available but each call requires user confirmation.

In none mode, no tools are available. The agent can only respond with text.

Filtering Built-in Tools

Any built-in can be allow-listed, blocked, or have its required permission overridden via the [tools] table in config.toml. See [tools] — built-in tool filters. Run meka tools list to see every built-in with its effective permission and current status.

MCP Tools

When MCP servers are configured, their tools are registered under a namespaced name of the form mcp__<server>__<tool> (e.g. mcp__notion__notion-search). The mcp__ prefix matches Claude Code’s convention and keeps MCP tools from colliding with built-in names. They appear in the system prompt catalogue alongside the built-ins — with their resolved permission level annotated inline — and are called the same way.

meka also exposes seven built-in MCP meta-tools for browsing server-side resources and prompts. All are deferred by default — call load_tool with the exact name to make the schema available on the next turn:

ToolPermissionDescription
list_mcp_resourcesReadList resources a server exposes
read_mcp_resourceReadRead a server resource by URI
list_mcp_promptsReadList server-defined prompts
get_mcp_promptReadRender a server prompt with arguments
subscribe_mcp_resourceReadReceive change notifications for a resource
unsubscribe_mcp_resourceReadStop receiving change notifications
list_mcp_resource_updatesReadInspect pending resource-change notifications

Scratchpad Parameter

All tools support an optional scratchpad string parameter. When provided, the tool’s output is saved to the scratchpad under that name instead of being returned inline. This lets the agent store large outputs for later processing without consuming conversation context.

execute_command({"command": "pdftotext doc.pdf -", "scratchpad": "pdf_text"})

How Tool Calls Work

  1. The agent receives your instruction and decides which tools to call
  2. For each tool call, meka checks the current permission level
  3. In ask mode, you are prompted to approve or deny each tool call
  4. If permitted, the tool executes and its output is fed back to the agent
  5. The agent may make additional tool calls or respond with text
  6. This loop continues until the agent has no more tool calls to make

Tool calls and their results are displayed in the terminal so you can see what the agent is doing.

todo_write

A built-in tool for managing a structured task list during a session. The agent uses this to track multi-step work and communicate progress. The task list is displayed in the terminal (for the main agent) and injected into the conversation context each turn. Calling todo_write replaces the entire list each time.

todo_read

Reads the current task list and returns it as plain text. The main agent normally sees the latest list via the per-turn context block, so todo_read mostly matters for sub-agents (whose context isn’t re-injected mid-loop) — but it’s available to any agent that wants to fetch its current list explicitly.

spawn_agent

Spawns a sub-agent to perform research, analysis, or any other delegated task. The sub-agent inherits the parent’s permission level, gets its own private todo list (todo_write / todo_read operate on the sub-agent’s own state), and cannot recursively spawn further sub-agents. The sub-agent runs silently — its tool calls are not surfaced to the terminal — and returns a single text report. Use this to keep exploratory or speculative work out of the main conversation context.

Multiple spawn_agent calls in one assistant turn run in parallel — useful when independent investigations can proceed concurrently.

skill

Loads a named skill’s instructions. Skills are user-defined knowledge packages stored in ~/.config/meka/skills/<name>/SKILL.md. The system prompt lists available skills with their description and when-to-use hint; the agent calls skill({"name": "<skill-name>"}) to load the full body. See Skills for how to author skills.

render_image

Displays an image the agent has in memory — as base64 bytes or in a scratchpad entry — as a multimodal content block. Complements fetch_url (network) and read_file (local file) by covering the third case: image data produced on the fly by a command pipeline.

Typical workflow:

execute_command({"command": "ffmpeg -i input.mp4 -vframes 1 -f image2pipe pipe: | base64 -w0", "scratchpad": "frame"})
render_image({"from_scratchpad": "frame"})

Parameters:

NameTypeRequiredDescription
from_scratchpadstringone of twoName of a scratchpad entry containing base64-encoded image bytes
base64stringone of twoBase64-encoded image bytes, passed inline

Exactly one of from_scratchpad or base64 must be provided. Prefer from_scratchpad for large images — inline base64 inflates tool-call JSON.

The bytes must decode to a supported raster image. PNG, JPEG, GIF, WebP, and BMP pass through unchanged; TIFF, ICO, HDR, EXR, TGA, PNM, QOI, DDS, and Farbfeld are auto-converted to PNG. Size cap is ~3.75 MB on the final payload.

Only call render_image when the current model supports vision input.

Redirecting output to the scratchpad

Several tools — execute_command, find_files, search_contents, fetch_url, spawn_agent — accept an optional scratchpad parameter that redirects their output to a named scratchpad entry instead of returning it inline. When this parameter is set, the tool produces its full, untruncated output: internal result-count caps (find_files 200, search_contents 100) and length caps (fetch_url max_length) are lifted for the scratchpad-bound result.

File Operations

read_file

Read the contents of a file at a given path. Supports text files and images.

Permission: Read

Parameters

NameTypeRequiredDescription
pathstringyesThe file path to read
offsetintegernoLine number to start reading from (0-based)
limitintegernoMaximum number of lines to read
regexstringnoReturn matching lines (capped — exact value advertised in the tool’s parameter schema) instead of a line range. Skipped for image files.
scratchpadstringnoSave output to the scratchpad under this name

Behavior

  • When offset and limit are both omitted, defaults to the first 2000 lines. If the file has more, a truncation notice is appended.
  • Use offset/limit to page through large files.
  • regex runs the pattern against each line and returns line:content rows (like grep -n). It bypasses offset/limit and is meaningless on image content.

Image files

Recognized image extensions are returned as base64-encoded multimodal content:

  • Provider-native (pass-through): .png, .jpg/.jpeg, .gif, .webp, .bmp
  • Convertible (decoded and re-encoded as PNG transparently): .tif/.tiff, .ico, .hdr, .exr, .tga, .pbm/.pgm/.ppm/.pnm, .qoi, .dds, .ff/.farbfeld
  • Unsupported (fall through to text read, which will fail on binary): .svg, .jxl, .heic, .avif

Images are rejected if the final payload exceeds 3.75 MB (~5 MB base64). Conversion can enlarge an image, so a small TIFF may produce a too-large PNG.

Only read image files when the current model supports vision input — text-only models will either error or silently drop the image block.

Examples

Read an entire file:

meka [r] > show me the contents of src/main.rs

Read lines 10-20:

meka [r] > show me lines 10 through 20 of src/main.rs

edit_file

Modify a file. Supports two modes: replace (swap old_string for new_string) and insert (place content before or after old_string while preserving the anchor). The file must have been read with read_file first (unless force is set).

Permission: Write

Parameters

NameTypeRequiredDescription
pathstringyesThe file path to edit
old_stringstringyesThe exact string to find (acts as anchor in insert modes)
new_stringstringone of threeReplace mode: replacement for old_string (an empty string deletes it)
insert_beforestringone of threeInsert mode: text inserted immediately before old_string (anchor preserved)
insert_afterstringone of threeInsert mode: text inserted immediately after old_string (anchor preserved)
replace_allbooleannoApply to every occurrence (default: false). If false and old_string matches more than once, the edit is rejected as ambiguous
forcebooleannoBypass read-before-edit requirement (default: false)
scratchpadstringnoSave output to the scratchpad under this name

Exactly one of new_string, insert_before, or insert_after must be provided. Mixing modes is rejected.

Behavior

  • If old_string matches more than once and replace_all is not set, the edit is rejected — add surrounding context to make the anchor unique, or set replace_all to change every occurrence.
  • To delete text, use replace mode with an empty new_string.
  • The file must have been previously read with read_file on the same path. This prevents blind edits. Set force to bypass this requirement.
  • If old_string is not found, the tool returns an error (without modifying the file).
  • On success, the response includes a small ±3-line snippet (with line numbers, lines truncated at 200 chars) around the first edited site so you can confirm the change landed without re-reading the file.

write_file

Create or overwrite a file with the given content.

Permission: Write

Parameters

NameTypeRequiredDescription
pathstringyesThe file path to write
contentstringyesThe content to write to the file
scratchpadstringnoSave output to the scratchpad under this name

Behavior

  • Creates parent directories if they do not exist.
  • Overwrites the file if it already exists.

Search Tools

find_files

Find files matching a glob pattern.

Permission: Read

Parameters

NameTypeRequiredDescription
patternstringyesGlob pattern to match files against
pathstringnoDirectory to search in (defaults to current directory)
scratchpadstringnoSave output to the scratchpad under this name

Behavior

  • Results are limited to 200 matches.
  • Returns one file path per line.

Glob Patterns

PatternMatches
*.rsAll .rs files in the current directory
**/*.rsAll .rs files recursively
src/*.txtAll .txt files in src/
test_*All files starting with test_

search_contents

Search file contents using a regex pattern. Powered by the ripgrep library.

Permission: Read

Parameters

NameTypeRequiredDescription
patternstringyesRegex pattern to search for
pathstringnoFile or directory to search in (defaults to current directory)
globstringnoGlob pattern to filter which files are searched (e.g., *.rs)
scratchpadstringnoSave output to the scratchpad under this name

Behavior

  • Searches recursively through directories.
  • Skips hidden files (starting with .) and common non-text directories (target, node_modules).
  • Results are limited to 100 matches.
  • Each result includes the file path, line number, and matching line.

Web Tools

fetch_url

Fetch a web page and return its content as markdown text.

Permission: Read

Parameters

NameTypeRequiredDescription
urlstringyesThe URL to fetch
max_lengthintegernoMaximum characters to return (default: 30000, 0 for no limit)
headersobjectnoCustom HTTP headers (overrides defaults like User-Agent)
regexstringnoIf provided, return only matching content (matches joined by newlines)
rawbooleannoReturn raw HTML instead of converting to markdown (default: false)
scratchpadstringnoSave output to the scratchpad under this name

Behavior

  • Fetches the page via HTTP GET.
  • Converts HTML to Markdown using fast_html2md (unless raw is true).
  • Truncates the output to max_length characters (default: 30,000).
  • HTTP timeout: 30 seconds.
  • Returns the HTTP status code as an error if the request fails (e.g., 404, 500).

Image URLs

If the response Content-Type is a supported raster image format, fetch_url returns a multimodal Image content block instead of markdown. No disk is touched — bytes are base64-encoded in memory.

Provider-native formats (passed through unchanged):

  • image/png, image/jpeg (and image/jpg), image/gif, image/webp, image/bmp (and image/x-ms-bmp)

Convertible formats (decoded and re-encoded as PNG transparently):

  • image/tiff, image/vnd.microsoft.icon / image/x-icon, image/vnd.radiance (HDR), image/x-exr, image/x-targa, image/x-portable-* (PNM), image/qoi, image/vnd.ms-dds, image/x-farbfeld

Unsupported formats (fall through to the text branch): image/svg+xml, image/jxl, image/heic, image/avif.

  • The max_length, regex, and raw options do not apply to image responses.
  • Size cap of ~3.75 MB applies to the output bytes (after conversion). Conversion can enlarge an image, so a 1 MB TIFF may produce a larger PNG.
  • Detection uses the response’s actual Content-Type header, so redirect chains and extension-less URLs are handled correctly.

Only fetch image URLs when the current model supports vision input — text-only models will either error or silently drop the image block.


Search DuckDuckGo and return the top results.

Permission: Read

Parameters

NameTypeRequiredDescription
querystringyesThe search query
headersobjectnoCustom HTTP headers (overrides defaults like User-Agent)
scratchpadstringnoSave output to the scratchpad under this name

Behavior

  • Returns up to 10 results per search.
  • Each result includes the title, source domain, URL, and a snippet with matched terms emphasised in bold.
  • Snippets are capped at 300 characters; use fetch_url on the result URL for the full page.
  • Uses HTML scraping (no API key required).
  • HTTP timeout: 30 seconds.

CAPTCHA detection

DuckDuckGo occasionally serves a bot-challenge page instead of results (detected by the anomaly-modal element). web_search returns a distinct error so the agent doesn’t silently retry:

DuckDuckGo served a CAPTCHA challenge (bot detection / rate limit).
Retry later.

If this happens often in your environment, configure a search-capable MCP server — see the MCP configuration examples for patterns that work well.

Shell Tool

execute_command

Execute a shell command and return its output.

Permission: Read (sandboxed) / Write (unsandboxed)

Parameters

NameTypeRequiredDescription
commandstringyesThe shell command to execute
timeout_msintegernoTimeout in milliseconds (default: 30000)
scratchpadstringnoSave output to the scratchpad under this name

Behavior

  • Executes the command via sh -c "<command>" on Unix, or powershell.exe -NoProfile -NonInteractive -Command "<command>" on Windows (same shell in both sandboxed and unsandboxed mode).
  • Captures both stdout and stderr.
  • Returns the exit code along with the output if non-zero.
  • Oversized output is losslessly persisted to the scratchpad by the agent layer — the tool itself never truncates.
  • Default timeout is 30 seconds. If the command exceeds the timeout, it is killed (on Unix, via the process group so backgrounded grandchildren are caught too).
  • Supports cancellation: pressing Ctrl+C while a command is running kills the child process.

Shell-specific semantics

  • Unix (sh -c): POSIX $VAR expansion applies. Pass a literal $ with single quotes ('$foo') or backslash escape (\$foo).
  • Windows (powershell.exe -Command): The script body reaches PowerShell directly. Use PowerShell syntax ($var = ..., $env:PATH) — and crucially, do not wrap your command in another powershell -Command "...". The outer PowerShell will expand your inner $var references to empty strings before the inner shell runs, producing a parser error on mangled syntax. If you need to invoke a nested script, drop it into a .ps1 file and run it by path, use -EncodedCommand <base64>, or escape each $ as `$.

Read-Only Sandbox

In read mode, commands run inside a sandbox that blocks writes to the user’s real data. Reads, program execution, and network access still work normally — the threat model is “no state mutation, but curl http://x | pdftotext must keep working.”

What’s blocked vs allowed (across all backends)

SurfaceBlockedAllowed
Filesystem writes outside tmp / Low-integrity paths
Filesystem reads
Program execution
Outbound network (TCP/UDP)
dbus / systemd-user state mutationsBubblewrap / macOSLandlock / Windows
Mach IPC state mutation (launchd, pasteboard, LaunchServices)macOSLinux / Windows
COM / RPC to Low-integrity-accepting services (Windows)
Inheritance of sensitive parent env vars (API keys, OAuth tokens, …)✓ (all platforms)

The sandbox is not an adversarial containment boundary — it’s defense-in-depth against an agent accidentally modifying user data. Set permission to none if you don’t trust a turn at all.

Environment variable scrubbing

Read-mode sandboxes still permit outbound network (the threat model intentionally keeps curl http://x | pdftotext-style pipelines working), so any secret in the parent process’s environment — ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, OAuth tokens, etc. — would be a live exfiltration vector under prompt injection. meka scrubs the child environment at spawn time across every backend (Bubblewrap, Landlock, Seatbelt, Windows Low-integrity).

  • Unix (Linux + macOS): allow-list. Only a curated set of vars survives into the read-mode child: PATH, HOME, USER, LOGNAME, SHELL, PWD, TERM, COLORTERM, LANG, TMPDIR, TMP, TEMP, plus everything matching the LC_* and XDG_* prefixes. Anything else is dropped — including credential-shaped vars (AWS_*, GITHUB_TOKEN, OPENAI_API_KEY, …) and credential-pointer vars (SSH_AUTH_SOCK, KUBECONFIG, GNUPGHOME, NETRC, GIT_ASKPASS, GIT_SSH_COMMAND, etc.) as well as benign-but-unlisted vars like EDITOR, PAGER, DISPLAY, custom toolchain vars, and so on. Unknown vars are dropped by default.

  • Windows: deny-list. PowerShell pulls in a long tail of system vars (PSModulePath, APPDATA, ProgramFiles, etc.) that don’t fit a tidy allow-list, so the Windows path lets everything through except names that match a heuristic deny-list. Dropped names include:

    • Credential-shaped substrings: *TOKEN*, *SECRET*, *PASSWORD*, *PASSPHRASE*, *API_KEY*, *_KEY*, *BEARER*, *CREDENTIAL*, etc.
    • Credential-pointer substrings: SSH_AUTH_SOCK, KUBECONFIG, GNUPGHOME, NETRC, GIT_ASKPASS, SSH_ASKPASS, GIT_SSH_COMMAND.
    • Provider / service prefixes: ANTHROPIC_*, OPENAI_*, AWS_*, GCP_*, GOOGLE_*, AZURE_*, GITHUB_*, OPENROUTER_*, GROQ_*, MISTRAL_*, COHERE_*, DATABASE_*, POSTGRES_*, MONGO_*, STRIPE_*, CLOUDFLARE_*, VAULT_*, OAUTH_*, JWT_*, SENTRY_*, SLACK_*, DISCORD_*, and others — see is_sensitive_env_name in src/sandbox.rs for the full list.

    The deny-list is intentionally aggressive on false positives (a legitimate GITHUB_ACTOR is dropped alongside GITHUB_TOKEN) because the cost of a missing env var is a confusing tool error, while the cost of a leaked credential is a live exfiltration channel.

Write mode keeps the full parent environment. Write mode is the trusted-operation path where users legitimately need NPM_TOKEN for npm publish, AWS_* creds for aws s3 cp, GH_TOKEN for gh pr create, etc. If you need a specific var inside a read-mode shell command, switch to write mode for that turn.

Linux: pick a backend

Linux supports two backends, selected via [shell].sandbox_backend in config.toml:

  • Bubblewrap (sandbox_backend = "bubblewrap", recommended): wraps the command in bwrap with --ro-bind /, tmpfs masks over /run, /tmp, /var/tmp, and $XDG_RUNTIME_DIR, plus --unshare-user --unshare-pid --unshare-uts --unshare-ipc. The tmpfs masks make the dbus session bus, systemd-user socket, and other socket-on-disk IPC paths unreachable, so systemctl --user start <unit>, dbus-send, and similar state-changing calls fail. Network is not unshared. Requires the bubblewrap package and a kernel with user-namespace creation enabled.
  • Landlock (sandbox_backend = "landlock", legacy / fallback): uses the Landlock LSM (kernel 5.13+). Blocks filesystem writes via landlock_restrict_self. Does not block dbus / systemd-user IPC, so a sandboxed shell can still invoke state-mutating dbus methods.

sandbox_backend is unset unless you pin it yourself — meka setup does not write it. When unset, meka probes Bubblewrap once at startup and prefers it when available, falling back to Landlock with a one-shot warning that points at the install path and the suppress-this-warning escape hatch.

[shell]
sandbox = true                       # default — set to false to disable
sandbox_backend = "bubblewrap"       # or "landlock"; unset = auto-detect

macOS and Windows

  • macOS: Uses sandbox-exec with a hardened SBPL profile (modeled after Codex’s vendored seatbelt policy, which is itself based on Chrome’s renderer sandbox). The profile is closed-by-default: filesystem writes are blocked, Mach-lookup is restricted to a curated allow-list of safe services, and mutation paths (launchd job control, pasteboard, LaunchServices, distributed notifications) are not in the allow-list. Network and DNS resolution remain available. The sandbox_backend config key is ignored.
  • Windows: Spawns the child with a duplicated primary token dropped to Low integrity (SECURITY_MANDATORY_LOW_RID) via SetTokenInformation(TokenIntegrityLevel, …). Writes to the home directory, %APPDATA%, Program Files, and system directories — any location with Medium-or-higher integrity ACLs — are blocked by the kernel. Low integrity also strips token privileges, and the same env scrubbing applied on Unix runs here (see Environment variable scrubbing above). The sandbox_backend config key is ignored.

Low integrity is not a total write-denial: the child can still write to the small residual Low-integrity-writable surface (%LOCALAPPDATA%\Low, %TEMP%\Low, any path with an explicit Low-integrity write ACE) and to files it creates itself.

When the configured backend is unavailable

If sandbox_backend = "bubblewrap" is set but bwrap isn’t on $PATH (or user namespaces are denied), execute_command in read mode returns a hard error rather than silently falling back. The error names the configured backend and the specific failure reason. Either install bubblewrap, set sandbox_backend = "landlock", or switch to write mode (Shift+Tab).

Disabling the sandbox entirely

To disable sandboxed shell execution in read mode altogether, set sandbox = false under [shell]. When disabled, shell commands require write mode.

[shell]
sandbox = false

Scratchpad

The scratchpad is a session-scoped working memory that the agent can use to store, retrieve, edit, and manage content without consuming conversation context. Entries are identified by string names and persist across turns within a session.

When the Scratchpad is Used

  • Proactively: The agent stores intermediate results (extracted text, API responses, research notes) for later use.
  • Via scratchpad parameter: Any tool can save its output directly to the scratchpad by including a scratchpad parameter in the tool call.
  • Automatically: When a tool’s output exceeds 30,000 characters, it is saved to the scratchpad under an auto-generated name (e.g., execute_command_1) and replaced with a preview in the conversation.

Tools

All five tools below ship default-active — no load_tool round-trip is required to use any of them.

scratchpad_write

Store content in the scratchpad. If the name already exists, the content is overwritten.

Permission: Read

NameTypeRequiredDescription
namestringyesName for the entry
contentstringyesThe content to store

scratchpad_read

Read or search a scratchpad entry by name.

Permission: Read

NameTypeRequiredDescription
namestringyesThe entry name
offsetintegernoCharacter offset to start reading from (default: 0)
limitintegernoMaximum characters to return; no hard cap — pass the entry’s size to load all content in one call. (Default and exact value are advertised in the tool’s parameter schema.)
regexstringnoSearch the entry and return matching lines (capped — exact value advertised in the tool’s parameter schema).

scratchpad_edit

Edit a scratchpad entry in place. Provide content for a full overwrite, or old_string/new_string for targeted replacement.

Permission: Read

NameTypeRequiredDescription
namestringyesThe entry name
contentstringnoFull replacement (mutually exclusive with old/new)
old_stringstringnoString to find
new_stringstringnoReplacement string
replace_allbooleannoReplace all occurrences (default: false)

scratchpad_list

List all scratchpad entries with their name, size, and creation time. No parameters.

Permission: Read

scratchpad_delete

Delete a scratchpad entry by name.

Permission: Read

NameTypeRequiredDescription
namestringyesThe entry name to delete

Lifecycle

  • Entries are scoped to the session and persist across turns.
  • Entries survive session compaction (/compact).
  • Entries are deleted when the session is deleted.
  • Two sessions can have entries with the same name without conflict.
  • Writing to an existing name overwrites it silently.