A lightweight personal AI assistant — single binary, zero dependencies.
For Ubuntu users, the APT repository is the recommended install path:
curl -fsSL https://kavilo-bot.github.io/homebrew-tap/apt/keyring.asc \
| sudo gpg --dearmor -o /usr/share/keyrings/kavilo-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/kavilo-archive-keyring.gpg] https://kavilo-bot.github.io/homebrew-tap/apt stable main" \
| sudo tee /etc/apt/sources.list.d/kavilo.list >/dev/null
sudo apt update
sudo apt install kaviloDownload the latest public binary mirror from Releases:
# macOS (Apple Silicon)
curl -LO https://github.com/kavilo-bot/homebrew-tap/releases/latest/download/kavilo_darwin_arm64.tar.gz
tar xzf kavilo_darwin_arm64.tar.gz
sudo mv kavilo /usr/local/bin/
# macOS (Intel)
curl -LO https://github.com/kavilo-bot/homebrew-tap/releases/latest/download/kavilo_darwin_amd64.tar.gz
tar xzf kavilo_darwin_amd64.tar.gz
sudo mv kavilo /usr/local/bin/
# Linux (x86_64)
curl -LO https://github.com/kavilo-bot/homebrew-tap/releases/latest/download/kavilo_linux_amd64.tar.gz
tar xzf kavilo_linux_amd64.tar.gz
sudo mv kavilo /usr/local/bin/kavilo is a Rust project (Cargo workspace). The supported toolchain is the
current stable Rust release (edition 2024).
cargo install --git https://github.com/kavilo-bot/kavilo --locked kavilo-clibrew install kavilo-bot/tap/kaviloAfter onboarding and configuration, macOS users can keep kavilo running in
the background at login with brew services start kavilo.
# Initialize configuration and workspace
kavilo onboard
# Add your API key to ~/.kavilo/config.json
# Get one at: https://openrouter.ai/keys
# Interactive chat
kavilo agent
# One-shot message
kavilo agent -m "What is the capital of France?"
# Start enabled channels and background services
kavilo start
# For long-running use, run it inside tmux so it survives terminal exit
tmux new -s kavilo
kavilo startFor a practical walkthrough of configuration, everyday usage, channels, MCP,
and troubleshooting examples, see docs/user-guide.md.
| Command | Description |
|---|---|
kavilo agent |
Interactive CLI agent |
kavilo agent -m "msg" |
One-shot message |
kavilo start |
Start enabled channels and background services |
kavilo onboard |
Initialize config and workspace |
kavilo profile list / use <name> / show / rm <name> |
Manage isolated ~/.kavilo/profiles/<name>/ homes (alias: kavilo instance ...) |
kavilo auth claude login / status / logout |
Authenticate with a Claude Pro/Max subscription via OAuth |
kavilo mcp slack login <alias> |
Log in to Slack MCP and save a named workspace alias |
kavilo mcp slack status [alias] |
Show Slack MCP alias status |
kavilo mcp slack logout <alias> |
Revoke a Slack MCP alias server-side and delete its saved token |
kavilo mcp login --server <name> |
Generic OAuth login for any RFC-compliant remote MCP server (Atlassian, Linear, Notion, …) |
kavilo mcp serve <self|slack> |
Run a built-in MCP server over stdio (for Cursor/Claude Desktop) |
kavilo status |
Show configuration status |
kavilo version |
Print version |
Most commands accept a global --profile/-p <name> flag (alias:
--instance/-i) that pins the KAVILO_HOME directory for that
invocation; see Instance profiles.
Configuration lives at ~/.kavilo/config.json. Key sections:
Current runtime status:
kavilo startruns enabled channels and background services.- The Rust runtime does not open an inbound HTTP listener.
- For persistent use, run it inside tmux.
{
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5",
"provider": "auto",
"maxTokens": 8192,
"workspace": "~/.kavilo/workspace"
}
},
"providers": {
"openrouter": { "apiKey": "sk-or-..." },
"anthropic": { "apiKey": "sk-ant-..." },
"openai": { "apiKey": "sk-..." }
},
"channels": {
"telegram": { "enabled": true, "token": "...", "allowFrom": ["*"] },
"slack": { "enabled": true, "botToken": "xoxb-...", "appToken": "xapp-...", "groupPolicy": "mention", "allowFrom": ["U..."] }
},
"runtime": {
"heartbeat": { "enabled": true, "intervalS": 1800 }
}
}kavilo can authenticate against an Anthropic Claude Pro or Max
subscription via OAuth instead of (or alongside) an sk-ant-... developer
API key. When you log in, requests for claude-* models route through
Anthropic's native /v1/messages endpoint with a bearer token and are
billed against your subscription quota — not the per-token developer
API meter.
# One-shot browser login. Drops a token at <KAVILO_HOME>/auth/anthropic/oauth.json.
kavilo auth claude login
# What's stored, when does it expire, who is it for?
kavilo auth claude status
# Best-effort revoke + delete the local file.
kavilo auth claude logoutThe token file is per-profile and lives next to mcp-auth/, so
kavilo --profile work and kavilo --profile personal keep separate
sessions.
The factory automatically prefers the OAuth path whenever a token is
present and the configured model contains claude or anthropic.
To force the API-key path even when an OAuth token exists (e.g. while
debugging a quota issue), set KAVILO_ANTHROPIC_OAUTH_DISABLED=1.
Caveat. Subscription-based auth for Claude isn't an officially documented integration path, so it can stop working without notice if Anthropic changes the OAuth surface. If
kavilo auth claude loginstops working, fall back to a developer API key (providers.anthropic.apiKeyin~/.kavilo/config.json). You can also pointKAVILO_ANTHROPIC_OAUTH_CLIENT_IDat your own OAuth client if you have one.
kavilo can also authenticate against an OpenAI ChatGPT Plus / Pro /
Team subscription via the same OAuth credentials the official
@openai/codex CLI
uses. Requests route through ChatGPT's Responses backend at
https://chatgpt.com/backend-api/codex/responses and are billed
against your subscription, not a developer API key.
# One-time setup: install the official CLI and run its OAuth login.
# This drops credentials at ~/.codex/auth.json (or $CODEX_HOME/auth.json).
npm install -g @openai/codex
codex loginThen opt in by setting provider = "openai_codex" in
~/.kavilo/config.json:
{
"agents": {
"defaults": {
"provider": "openai_codex",
"model": "gpt-5.5",
"reasoningEffort": "high"
}
}
}reasoningEffort accepts "low" | "medium" | "high" and maps to the
Responses API's reasoning.effort field. The provider supports
streaming, tool calls, prompt caching (via a deterministic
prompt_cache_key hashed from the canonicalized message array), and
returns the input/output/reasoning token counts on kavilo usage.
If ~/.codex/auth.json is missing or stale, kavilo surfaces the error
with a "run codex login" hint instead of failing opaquely.
When kavilo start is running, it keeps the Codex access token in
~/.codex/auth.json (or $CODEX_HOME/auth.json) alive so an idle
runtime never wakes up to a 401. The access token is a JWT good for
~10 days; kavilo decodes its exp claim once an hour and refreshes
anything that expires within the next 24 hours via the same OAuth
exchange the official @openai/codex CLI uses:
POST https://auth.openai.com/oauth/token
Content-Type: application/x-www-form-urlencoded
client_id=app_EMoamEEZ73f0CkXaXp7hrann&grant_type=refresh_token&refresh_token=<rt>
The client ID and endpoint were extracted from the codex CLI binary
itself. On success kavilo atomically rewrites auth.json with the
rotated access / refresh / id tokens (preserving every other key like
OPENAI_API_KEY and auth_mode), bumps last_refresh, and the
provider's per-request auth.load() picks up the new bearer on the
next chat call. No restart needed.
Skipped automatically:
- No
auth.jsoncached (runcodex loginonce). auth.jsonhas norefresh_token(very oldcodex loginshape) — recover withcodex login --force.
Disable the loop entirely:
export KAVILO_CODEX_REFRESH_DISABLED=1Tune the cadence:
"codex": {
"enabled": true,
"refreshIntervalS": 3600,
"refreshLeadS": 86400
}Override the OAuth endpoint (mirrors the CLI's
CODEX_REFRESH_TOKEN_URL_OVERRIDE env var):
export CODEX_REFRESH_TOKEN_URL_OVERRIDE=https://your-proxy.example/oauth/tokenIf the refresh token itself gets revoked server-side — admin
session-policy change, manual codex logout from another device,
client-ID rotation upstream — kavilo logs a RefreshFailed line every
tick and the next agent turn will surface the existing "Re-run codex login and retry" message. Auto-refresh extends the supported window
but does not turn an invalidated grant into a recoverable one.
Caveat. Like the Claude Pro path, this is not an officially documented OpenAI integration. The
chatgpt.comCodex backend can shift its protocol without notice, and theapp_EMoamEEZ73f0CkXaXp7hrannclient ID is hard-coded — if OpenAI rotates either, auto-refresh stops working and you fall back to interactivecodex login. If kavilo starts returning 401 after working previously, re-runcodex login; if the wire format changes, fall back to a developer API key on the standardopenaiprovider.
An instance profile is an isolated KAVILO_HOME directory with its own
config, sessions, MCP auth, cron jobs, and workspace — think of it as one
self-contained kavilo "instance". The default instance profile lives at
~/.kavilo/ itself; named ones live under ~/.kavilo/profiles/<name>/.
The CLI accepts both spellings interchangeably: kavilo profile ... and
kavilo instance ... are the same subcommand, and --profile/-p and
--instance/-i are the same flag.
# Create / switch to an instance profile (sticky across shells)
kavilo profile use work # or: kavilo instance use work
kavilo profile show # which instance profile is active and where it lives
kavilo profile list # all instance profiles in ~/.kavilo/profiles
kavilo profile rm scratch # delete an instance profile (asks for confirmation)
# Or override per-invocation without changing the sticky profile
kavilo --profile work agent # or: kavilo --instance work agent
KAVILO_HOME=/path/to/home kavilo agentResolution order, evaluated once at process start and pinned for the lifetime of that process:
KAVILO_HOMEenv (set explicitly or by--profile)- Sticky
~/.kavilo/active_profile - The default
~/.kavilo/
If KAVILO_HOME is already set in your shell, it wins over --profile
and kavilo prints a warning when the two disagree. A kavilo profile use <other> issued while kavilo start or kavilo mcp serve … is already
running does not affect that running process; the active instance
profile is fixed for the lifetime of each process.
kavilo profile rm refuses to delete the default instance profile
(which contains every named profile under it) and refuses to delete the
active instance profile.
Per-profile (lives under <KAVILO_HOME>/): config.json, the workspace
(sessions, AGENTS.md, skills, memory), mcp-auth/slack/<alias>.{json,app.json},
cron/jobs.json, media/, usage/, logs/, CLI history.
Process-global, shared across profiles:
- Provider env vars (
ANTHROPIC_API_KEY,OPENAI_API_KEY,GROQ_API_KEY, …) andKAVILO_*overlays (KAVILO_MODEL,KAVILO_PROVIDER,KAVILO_API_KEY,KAVILO_API_BASE,KAVILO_WORKSPACE). These override any value placed in a profile'sconfig.json. If you want different keys per profile, leave them out of your shell environment and put them in each profile'sconfig.jsoninstead. - The sticky profile file at
~/.kavilo/active_profile(singleton). - The Slack OAuth loopback port (
127.0.0.1:7898).
tools.restrictToWorkspace(defaulttrue) is a correctness boundary, not an adversarial one. It preventsread_file/write_file/list_directory/glob_filesfrom touching paths outside the workspace, but theshelltool still runssh -cwith full host access.- For genuinely untrusted execution, run
kaviloitself inside a container / VM / seccomp jail. tools.exec.scrubEnv = true(off by default) drops*_API_KEY,*_API_TOKEN,*_SECRET, andKAVILO_*from theshelltool's child env so the agent cannot exfiltrate provider credentials viaprintenv. Add more patterns viatools.exec.scrubEnvExtra.
kavilo mcp serve <name> runs a built-in server over stdio so editors like
Cursor or Claude Desktop can use kavilo's own state and Slack tools without
running kavilo start. Two servers are shipped:
self— exposes status, sessions, channels, MCP servers, cron jobs, token usage, channel send, and the generic MCP OAuth login flow. Sensitive fields (auth tokens, webhook secrets, API keys) are sanitized in responses.slack— exposes whoami, channel history and replies, message / user / file search, user listing and lookup, permalinks, posting as the authenticated user, and DM open/self-DM, against the workspace whose alias is configured.
Example Claude Desktop / Cursor entry:
{
"mcpServers": {
"kavilo-self": {
"command": "kavilo",
"args": ["mcp", "serve", "self"]
}
}
}The same tools are also registered in-process when you run kavilo agent or
kavilo start, so the agent can call them without spawning a child process.
For a detailed system design overview, see docs/architecture.md.
Maintainers: see docs/releasing.md for the tag-driven release automation and
public Homebrew/binary mirror flow.
kavilo connects to Slack over Socket Mode — no public HTTPS redirect or
port forwarding required. A Slack DM or @bot mention becomes an inbound
prompt to the agent; replies post back into the same thread. This is
distinct from Slack MCP (next section), which is a tool surface the agent
calls into.
"channels": {
"slack": {
"enabled": true,
"botToken": "xoxb-...",
"appToken": "xapp-...",
"groupPolicy": "mention",
"allowFrom": ["U..."]
}
}For the full end-to-end setup, scope list, and troubleshooting guide, see
docs/slack-channel-setup.md. It also covers the "kavilo bot ↔ kavilo
bot in a private channel" multi-agent coordination pattern.
kavilo can log into Slack MCP with a named alias and store the OAuth
token outside config.json, injecting the bearer token only at runtime.
You bring your own Slack app — kavilo does not ship a hosted client.
Register a Slack app, capture its client_id and client_secret, and
add http://127.0.0.1:7898/oauth/callback as a redirect URI (Slack
accepts loopback HTTP for this). Then log in:
kavilo mcp slack login workspace-alias \
--client-id YOUR_SLACK_CLIENT_ID \
--client-secret YOUR_SLACK_CLIENT_SECRET \
--team YOUR_TEAM_ID
# Inspect all Slack aliases or one alias
kavilo mcp slack status
kavilo mcp slack status workspace-alias
# Revoke server-side and remove the saved token
kavilo mcp slack logout workspace-alias
# or skip the revoke call (e.g. token is already invalid):
kavilo mcp slack logout workspace-alias --local-onlyAfter the first successful login, --client-id / --client-secret
are persisted to ~/.kavilo/mcp-auth/slack/<alias>.app.json; subsequent
logins for the same alias don't need them.
If your Slack app must use an HTTPS redirect URI (some workspaces enforce
this), point it at any small page you control that 302-redirects to
http://127.0.0.1:7898/oauth/callback and pass that HTTPS URL as
--redirect-uri. docs/slack-mcp-setup.md has the full end-to-end
setup, including a sample GitHub Pages relay.
Saved tokens live under ~/.kavilo/mcp-auth/slack/.
When kavilo start is running, it keeps every Slack-provider remote MCP
server's user token fresh so the agent's MCP calls never go out with an
expired bearer — even if the runtime sits idle for hours.
Two complementary mechanisms drive this:
- Periodic background refresh. Every 30 minutes (configurable via
KAVILO_SLACK_REFRESH_TICK_SECS) the runtime walks eachtools.mcpServers.<alias>entry whoseauth.provider == "slack", loads~/.kavilo/mcp-auth/slack/<alias>.json, and — when the cachedexpires_atis inside the 60s leeway window and arefresh_tokenis on file — calls Slack'soauth.v2.accesswithgrant_type=refresh_token, then atomically rewrites the cache file with the new access token, the rotated refresh token, and a freshexpires_at. - Live
Authorizationheader per request. Each remote Slack MCP client re-reads<alias>.jsonon every outbound MCP call instead of pinning the bearer at startup. So the moment the background refresher rewrites the file, the next MCP request uses the new token without forcing a reconnect.
The runtime also runs one proactive refresh at startup (inside
build_slack_auth_provider) so the very first MCP request after a
long-idle window never goes out with a stale bearer.
Skipped automatically:
- Aliases with no
<alias>.jsoncached (runkavilo mcp slack login <alias>first). - Legacy non-rotating tokens (no
expires_atrecorded). Slack treats these as long-lived; if the workspace admin eventually invalidates one, runkavilo mcp slack login <alias> --forceto re-auth. - Tokens whose
refresh_tokenis missing — kavilo cannot recover these silently and logs a warning pointing you atkavilo mcp slack login <alias> --force. - Disabled servers (
enabled: falseintools.mcpServers.<alias>).
Disable the loop entirely:
export KAVILO_SLACK_REFRESH_DISABLED=1 # or any of: 1/true/TRUE/yes/YESTune the tick:
# Refresh every 5 minutes instead of every 30
export KAVILO_SLACK_REFRESH_TICK_SECS=300Caveats:
- This only applies to
kavilo start.kavilo agent(one-shot) andkavilo mcp serve slackrefresh opportunistically on each Slack API call instead, which is sufficient for short-lived processes. - The refresh token itself can expire if Slack's app or workspace
policy says so (e.g. token rotation revoked, scope change, admin
session policy). When that happens you'll see a
RefreshFailedlog line and the next MCP call will surfaceMCP_AUTH_REQUIRED:server=<alias>; reason=invalid_tokento the agent — re-auth withkavilo mcp slack login <alias> --force.
The config entry written by kavilo mcp slack login ... looks similar to
this:
{
"tools": {
"mcpServers": {
"workspace-alias": {
"type": "http",
"url": "https://mcp.slack.com/mcp",
"enabledTools": [
"slack_search_channels",
"slack_search_public",
"slack_search_public_and_private",
"slack_search_users",
"slack_read_channel",
"slack_read_thread",
"slack_send_message",
"slack_send_message_draft"
],
"auth": {
"provider": "slack",
"teamId": "YOUR_TEAM_ID",
"label": "My Workspace"
}
}
}
}
}Every tool from a remote MCP shows up to the LLM as
mcp_<alias>_<tool> with whatever description the upstream server
ships. Slack's hosted MCP, for example, ships generic descriptions like
"Search public Slack messages" — with no hint about which
workspace or whose identity. When you wire two slack-shaped surfaces
into one agent (a Foothill Mac bot via the built-in slack_* tools and
a Zillow Slack user-OAuth via a remote MCP), the model has nothing to
disambiguate them with and thrashes between the two.
Set tools.mcpServers.<alias>.description to a single sentence of
routing context and kavilo will prepend it to every tool description
exposed by that server, in the form:
[server: <your text>]
<upstream tool description>
Example:
"zillow_user": {
"type": "http",
"url": "https://mcp.slack.com/mcp",
"description": "Acts as your human user account in the Zillow Slack workspace (T024JJ69R). Use these tools when you need to search across Zillow channels, read Zillow threads, or act on your behalf in Zillow Slack. Do NOT use these for the Foothill Mac workspace where kavilo runs as the @Foothill Mac bot.",
"auth": { "provider": "slack", "teamId": "T024JJ69R", "label": "Zillow Group" },
"enabledTools": ["slack_search_public_and_private", "slack_read_thread"]
}Notes:
- Empty / whitespace-only descriptions are treated as opt-out and the tool description goes through unchanged. Existing configs keep their current behavior.
- The prefix applies uniformly to every tool the server exposes — write the description once, not per tool.
- Equally useful for non-Slack MCPs: tag
atlassian-jirawith the Atlassian site URL,glean_defaultwith the corp tenant, etc., so the model has a one-line answer to "what is this server for?".
The OpenAI-compatible HTTP client (crates/kavilo-providers/src/openai_compat.rs,
shared by deepseek, openai, dashscope, etc.) splits its per-request timeout
into two values and rotates onto a pool-disabled client when it suspects a
half-open socket. This was introduced after a single Slack-driven agent turn
spent 22 minutes hanging across 11 LLM iterations — every iteration hit the
old uniform 120 s timeout on a stale TCP connection that the upstream LB had
silently dropped, then succeeded on the immediate retry.
CHAT_REQUEST_TIMEOUT_SECS(default 45 s) bounds non-streaming chat completions. Healthy upstreams reply in well under 30 s; anything past this is overwhelmingly a dead socket, so kavilo aborts and retries fast rather than wasting two minutes per iteration.STREAM_REQUEST_TIMEOUT_SECS(default 180 s) is intentionally generous — it covers the entire SSE stream lifetime, not just headers.- When a retry is triggered by a timeout or connect failure (
is_timeout()/is_connect()), the next attempt is sent throughoneshot_client, built withpool_max_idle_per_host(0), so it cannot pick the same half-open socket out of the pool. Retries triggered by5xx/429status keep using the warm pooled client because the connection is fine. - After a successful retry, the next chat call goes back to the pooled client; the no-pool path only pays its handshake cost on actual failures.
You'll see fresh_retry=true in the transient network error; retrying after backoff log line when this path activates — that's the signal the
fix is working as intended on a flaky network.
When kavilo start is running, it keeps every already-logged-in AWS SSO
session alive so the agent's shell tool, sub-shells, and any AWS SDK
process running under the same user always see a non-expired access
token — and so the rolling 90-day refresh-token window never lapses
even if the machine sits idle for weeks.
Every 5 minutes (configurable) the runtime walks each
[sso-session <name>] block in ~/.aws/config. For any session whose
cached access token in ~/.aws/sso/cache/ is within 10 minutes of
expiry and has a refreshToken, kavilo runs:
aws sso-oidc create-token \
--client-id <cached> --client-secret <cached> \
--grant-type refresh_token --refresh-token <cached> \
--region <cached>…and atomically rewrites the cache file with the new accessToken,
new refreshToken, and an expiresAt of now + expiresIn. The
refresh runs silently in well under a second — no browser, no
interactive prompt. kavilo never initiates a fresh login itself, so
each SSO session must have been logged in at least once manually.
Recommended ~/.aws/config shape (all SSO profiles share one
[sso-session] block so they all stay alive together):
[sso-session corp]
sso_start_url = https://corp.awsapps.com/start
sso_region = us-east-1
sso_registration_scopes = sso:account:access
[profile dev]
sso_session = corp
sso_account_id = 111111111111
sso_role_name = Developer
region = us-east-1
[profile prod]
sso_session = corp
sso_account_id = 222222222222
sso_role_name = ReadOnly
region = us-east-1Caveats:
- Legacy SSO profiles (the kind written under
[profile X]withsso_start_urldirectly and no[sso-session …]block) have no refresh token and can only be re-authenticated interactively. kavilo skips them with a warning. - The
awsCLI must be onPATH. If it isn't, the refresher logs a warning and disables itself for that run.
Defaults and how to disable:
"aws": {
"enabled": true,
"refreshIntervalS": 300,
"refreshLeadS": 600
}Set aws.enabled = false in ~/.kavilo/config.json or export
KAVILO_AWS_REFRESH_DISABLED=1 to turn the loop off entirely.
- Multi-provider LLM support — OpenAI, Anthropic, Azure, Groq, DeepSeek, Gemini, Ollama, and many more via OpenAI-compatible API
- Chat channels — Telegram and Slack, with group policies, media support, and per-sender allow-lists
- Built-in tools — File operations, shell execution, web search/fetch, message sending, subagent spawning
- MCP support — Connect to Model Context Protocol servers (stdio and HTTP transports)
- Memory system — Long-term memory (MEMORY.md) and history with LLM-driven consolidation
- Skills — Workspace and built-in skills with frontmatter configuration
- Cron scheduler — Schedule recurring tasks and one-shot reminders
- Heartbeat service — Periodic task checking via HEARTBEAT.md
- Token tracking — Per-call usage logging with trip/total counters
kavilo stores its runtime data in ~/.kavilo/. For long-running use,
start kavilo start inside tmux so it survives terminal exit:
tmux new -s kavilo
kavilo startOn macOS, brew services start kavilo will keep it running at login.
kavilo is a Cargo workspace (Rust 2024). Use the included Makefile or
plain cargo invocations:
git clone https://github.com/kavilo-bot/kavilo.git
cd kavilo
make build # Release build of the kavilo binary for the host
make test # cargo test --workspace
make clippy # cargo clippy --workspace --all-targets -- -D warnings
make fmt # cargo fmt --all -- --check
make cross # Cross-compile for all published targets
make deb # Build a .deb for the host architecture (cargo-deb)
make install # cargo install --path crates/kavilo-cliMIT