freshcrate
Home > AI Agents > kai

kai

Agentic AI assistant on Telegram, powered by Claude Code. Runs locally with shell access, spec-driven PR reviews, layered security, persistent memory, and scheduled jobs. Your machine, your data, your

Description

Agentic AI assistant on Telegram, powered by Claude Code. Runs locally with shell access, spec-driven PR reviews, layered security, persistent memory, and scheduled jobs. Your machine, your data, your rules.

README

Kai

CI Python 3.13+ License Version

Kai is an AI agent, not a chatbot. It manages persistent agent subprocesses running on your hardware - Claude Code by default, with Goose as an alternative backend - and connects them to Telegram as a control surface. Shell, filesystem, git, web search, scheduling - the agent has real access to your system and can take real action on it. It reviews PRs when code is pushed, triages issues when they're opened, monitors conditions on a schedule, and operates across any project on your machine. Multiple users can share a single Kai instance, each with their own isolated subprocess, workspace, conversation history, and optional OS-level process separation.

For detailed guides on setup, architecture, and optional features, see the Wiki.

Architecture

Kai is two layers: an outer Python application that handles Telegram, HTTP, and scheduling, and one or more inner agent subprocesses that do the thinking and acting. The outer process programs against an AgentBackend ABC (backend.py), so it manages lifecycle, authentication, and transport identically regardless of which backend is running. Claude Code is the default; Goose ACP is an alternative. Both follow the same lifecycle: one subprocess per user, lazy creation, idle eviction. Resource usage scales with active users, not registered ones.

This is what separates Kai from API-wrapper bots that send text to a model endpoint and relay the response. The inner agent is a full agentic runtime - it reads files, runs shell commands, searches the web, writes and commits code, and maintains context across a session. Claude Code and Goose each bring their own model ecosystem. Kai gives the runtime a durable home, a scheduling system, event-driven inputs, persistent memory, and a security model designed around the fact that it has all of this power.

Everything runs locally. Conversations never transit a relay server. Voice transcription and synthesis happen on-device. API keys are proxied through an internal service layer so they never appear in conversation context. There is no cloud component between you and your machine.

Security model

Giving an AI agent shell access is a real trust decision. Kai's approach is layered defense - each layer independent, so no single failure is catastrophic:

  • Telegram auth - only explicitly whitelisted user IDs can interact. Unauthorized messages are silently dropped before reaching any handler.
  • TOTP gate (optional) - two-factor authentication via time-based one-time passwords. After a configurable idle timeout, Kai requires a 6-digit authenticator code before processing anything. The secret lives in a root-owned file (mode 0600) that the bot process cannot read directly; it verifies codes through narrowly-scoped sudoers rules. Even if someone compromises your Telegram account, they can't use your assistant without your authenticator device. Rate limiting with disk-persisted lockout protects against brute force.
  • Process isolation - authentication state lives in the bot's in-memory context, not in the filesystem or conversation history. The inner agent process cannot read, manipulate, or bypass the auth gate.
  • Path confinement - file exchange operations are restricted to the active workspace via Path.relative_to(). Traversal attempts are rejected. The send-file API also blocks access to history and memory directories.
  • Service proxy - external API keys live in server-side config (services.yaml) and are injected at request time. The agent calls APIs through a local proxy endpoint; the keys never enter the conversation. Per-service SSRF controls prevent services from being used as open HTTP proxies.
  • Multi-user isolation - each user's data is namespaced by chat ID: separate conversation history, workspace state, scheduled jobs, and file storage. When os_user is configured in users.yaml, the inner agent subprocess runs as a dedicated OS account via sudo -u, creating a hard process-level boundary between users and between the bot and the AI.

Setup for TOTP requires the optional dependency group and root access:

pip install -e '.[totp]'             # adds pyotp and qrcode
sudo python -m kai totp setup        # generate secret, display QR code, confirm

For the full architecture, see System Architecture. For TOTP details, see TOTP Authentication.

Features

Workspaces

Switch the agent between projects on your system with /workspace <name>. Names resolve relative to WORKSPACE_BASE (set in .env). Identity and memory carry over from the home workspace, so Kai retains full context regardless of what it's working on. Create new workspaces with /workspace new <name>. Absolute paths are not accepted - all workspaces must live under the configured base directory.

Per-workspace configuration is supported via workspaces.yaml (or /etc/kai/workspaces.yaml for protected installations). Each workspace can override the model, budget, timeout, environment variables, and system prompt. See workspaces.example.yaml for the full format.

Multi-user

A single Kai instance can serve multiple Telegram users, each fully isolated. Define users in users.yaml (or /etc/kai/users.yaml for protected installations):

users:
  - telegram_id: 123456789
    name: alice
    role: admin           # receives webhook notifications (GitHub, generic)
    github: alice-dev     # routes GitHub events to this user
    os_user: alice        # subprocess runs as this OS account
    home_workspace: /home/alice/workspace
    max_budget: 15.0      # ceiling for /settings budget (CLAUDE_MAX_BUDGET_USD is the default)

Each user gets:

  • Own agent subprocess - created lazily on first message, evicted after idle timeout (CLAUDE_IDLE_TIMEOUT, default 30 minutes). No shared conversation state.
  • Isolated data - conversation history, workspace settings, scheduled jobs, and file uploads are all namespaced by user. One user cannot see or affect another's state.
  • Optional OS-level separation - set os_user to run that user's agent process as a dedicated system account via sudo -u. Requires a sudoers rule (the install script generates one automatically).
  • Per-user home workspace - each user can have their own default workspace directory.
  • Role-based routing - admins receive unattributed webhook events (GitHub pushes, generic webhooks). Regular users interact only through Telegram messages.

Run make config to generate users.yaml, or create one manually from users.example.yaml. See the Multi-User Setup wiki page for the full field reference. When users.yaml is absent, Kai falls back to ALLOWED_USER_IDS for backward compatibility. If neither is set, Kai refuses to start (fail-closed). The CLAUDE_USER env var acts as a global fallback for subprocess isolation; per-user os_user in users.yaml takes precedence when set.

Memory

Three layers of persistent context give the agent continuity across sessions:

  1. Identity (home/.claude/CLAUDE.md) - voice, rules, and operational guidelines. Lives in the home workspace. When the agent switches to a foreign workspace, Kai injects it so behavior stays consistent. In the home workspace, the agent reads it natively.
  2. Home memory (DATA_DIR/memory/MEMORY.md) - personal memory, always injected regardless of current workspace. Proactively updated by Kai.
  3. Conversation history (DATA_DIR/history/<chat_id>/) - JSONL logs, one file per day per user. Searchable for past conversations.

Workspaces can also define a system prompt via workspaces.yaml for workspace-specific instructions. See System Architecture.

Scheduled jobs

Reminders and recurring agent jobs with one-shot, daily, and interval schedules. Ask naturally ("remind me at 3pm") or use the HTTP API (POST /api/schedule). Agent jobs run as full agent sessions - Kai can check conditions, search the web, run commands, and report back on a schedule. Auto-remove jobs support monitoring use cases where the agent watches for a condition and deactivates itself when it's met. See Scheduling and Conditional Jobs.

PR Review Agent

When code is pushed to a pull request, Kai automatically reviews it. A one-shot agent subprocess analyzes the diff, checks for bugs, style issues, and spec compliance, and posts a review comment directly on the PR. If you push fixes, it reviews again - and checks its own prior comments so it doesn't nag about things you already addressed. See PR Review Agent.

Issue Triage Agent

When a new issue is opened, Kai triages it automatically. A one-shot agent subprocess reads the issue, applies labels (creating them if they don't exist), checks for duplicates and related issues, assigns it to a project board if appropriate, posts a triage summary comment, and sends you a Telegram notification. See Issue Triage Agent.

Both agents are fire-and-forget background tasks that run independently of your chat session. They use separate agent processes, so a review or triage can happen while you're mid-conversation. Opt-in per-user via /github reviews on and /github triage on, or in users.yaml. The PR_REVIEW_ENABLED and ISSUE_TRIAGE_ENABLED env vars still work as global fallbacks.

GitHub notification routing

GitHub event notifications (pushes, PRs, issues, comments, reviews) can be routed to a separate Telegram group via /github notify <chat_id> (or the deprecated GITHUB_NOTIFY_CHAT_ID env var as a global fallback), keeping your primary DM clean for conversation. Agent output (review comments, triage summaries) also routes to the group. See GitHub Notification Routing.

Webhooks

An HTTP server receives external events and routes them to the agent. GitHub webhooks (pushes, PRs, issues, comments, reviews) are validated via HMAC-SHA256. A generic endpoint (POST /webhook) accepts JSON from any service - CI pipelines, monitoring alerts, deployment hooks, anything that can POST JSON. See Exposing Kai to the Internet.

File exchange

Send any file type directly in chat - photos, documents, PDFs, archives, anything. Files are saved to per-user directories under the data directory with timestamped names, and the agent gets the path so it can work with them via shell tools. Kai can also send files back to you through the internal API. Images render inline; everything else arrives as a document attachment. Set FILE_RETENTION_DAYS to automatically clean up old uploads.

Streaming responses

Responses stream into Telegram in real time, updating the message every 2 seconds.

Model switching

Switch models via /models (interactive picker) or /model <name> (direct). Available models depend on the configured backend: Claude Code offers Opus, Sonnet, and Haiku; Goose offers whatever the user's configured provider supports. Changing models restarts the session.

Voice input

Voice notes are transcribed locally using whisper.cpp and forwarded to the agent. Requires ffmpeg and whisper-cpp. Disabled by default - set VOICE_ENABLED=true after installing dependencies. See the Voice Setup wiki page.

Voice responses (TTS)

Text-to-speech via Piper TTS. Three modes: /voice only (voice note, no text), /voice on (text + voice), /voice off (text only, default). Eight curated English voices. Requires pip install -e '.[tts]' and TTS_ENABLED=true. See Voice Setup.

Dual-mode Telegram transport

Kai supports two ways of receiving Telegram updates: long polling (default) and webhooks. Polling works out of the box behind NAT with zero infrastructure. Set TELEGRAM_WEBHOOK_URL in .env to switch to webhook mode for lower latency - this requires a tunnel or reverse proxy (see Exposing Kai to the Internet).

Crash recovery

If interrupted mid-response, Kai notifies you on restart and asks you to resend your last message.

Commands

Command Description
/new Clear session and start fresh
/stop Interrupt a response mid-stream
/models Interactive model picker
/model <name> Switch model (available models depend on backend)
/settings Show per-user settings (model, budget, timeout, context window)
/settings <field> <value> Change a setting (model, budget, timeout, context)
/settings reset [field] Clear all overrides, or one field
/workspace (or /ws) Show current workspace
/workspace <name> Switch by name (resolved under WORKSPACE_BASE)
/workspace home Return to default workspace
/workspace new <name> Create a new workspace with git init
/workspace allow <path> Add an allowed workspace for your user
/workspace deny <path> Remove an allowed workspace for your user
/workspace allowed List your allowed workspaces
/workspaces Interactive workspace picker
/github Show GitHub notification settings
/github notify <chat_id> Route your GitHub notifications to a specific chat
/github reviews on|off Enable or disable the PR review agent for you
/github triage on|off Enable or disable the issue triage agent for you
/github add <owner/repo> Subscribe to a repo (auto-registers webhook if token set)
/github remove <owner/repo> Unsubscribe from a repo
/github token <token> Store GitHub PAT for auto-webhook registration (delete the message after sending)
/voice Toggle voice responses on/off
/voice only Voice-only mode (no text)
/voice on Text + voice mode
/voice <name> Set voice
/voices Interactive voice picker
/stats Show session info, model, and cost
/job (or /jobs) List scheduled jobs
/job info <id> Show job details
/job cancel <id> Cancel a scheduled job
/webhooks Show webhook server status
/help Show available commands

Requirements

Setup

git clone git@github.com:dcellison/kai.git
cd kai
python3 -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
cp .env.example .env

Environment variables

Variable Required Default Description
TELEGRAM_BOT_TOKEN Yes Bot token from BotFather
ALLOWED_USER_IDS Deprecated* Comma-separated Telegram user IDs. Superseded by users.yaml; use make config instead. (*Still works if users.yaml absent.)
CLAUDE_MODEL Deprecated* sonnet Default model. Set per-user in users.yaml or via /settings model.
CLAUDE_TIMEOUT_SECONDS Deprecated* 120 Per-message timeout. Set per-user in users.yaml or via /settings timeout.
CLAUDE_MAX_BUDGET_USD Deprecated* 10.0 Session budget cap. Set per-user in users.yaml or via /settings budget.
CLAUDE_MAX_CONTEXT_WINDOW Deprecated* 0 Context window size in tokens (0 = backend default). Set per-user in users.yaml or via /settings context.
CLAUDE_AUTOCOMPACT_PCT No 80 Context compression threshold %, Claude Code only. When usage hits this, Claude compresses history. Can only lower the default (~83%), not raise it.
CLAUDE_MAX_SESSION_HOURS No 0 Maximum session age in hours before recycling the subprocess (0 = no limit). Recommended: 4-8 on memory-constrained machines.
WORKSPACE_BASE Deprecated* Base directory for workspace name resolution. Set per-user in users.yaml.
ALLOWED_WORKSPACES Deprecated* Comma-separated extra workspace paths. Users now manage their own via /workspace allow.
WEBHOOK_PORT No 8080 HTTP server port for webhooks and scheduling API
WEBHOOK_SECRET No Secret for webhook validation and scheduling API auth
TELEGRAM_WEBHOOK_URL No Telegram webhook URL (enables webhook mode; omit for polling)
TELEGRAM_WEBHOOK_SECRET No Separate secret for Telegram webhook auth (defaults to WEBHOOK_SECRET)
PR_REVIEW_ENABLED Deprecated* false Enable automatic PR review globally. Set per-user in users.yaml or via /github reviews.
PR_REVIEW_COOLDOWN No 300 Minimum seconds between reviews of the same PR. Machine-wide resource limit, not per-user.
SPEC_DIR No specs Spec directory relative to repo root, for branch-name matching in PR reviews
ISSUE_TRIAGE_ENABLED Deprecated* false Enable automatic issue triage globally. Set per-user in users.yaml or via /github triage.
GITHUB_NOTIFY_CHAT_ID Deprecated* Global fallback for GitHub notification routing. Set per-user in users.yaml or via /github notify.
CLAUDE_USER Deprecated* Global OS user for subprocess isolation. Set per-user via os_user in users.yaml.
CLAUDE_IDLE_TIMEOUT No 1800 Seconds before idle subprocesses are evicted (0 to disable)
VOICE_ENABLED No false Enable voice message transcription
TTS_ENABLED No false Enable text-to-speech voice responses
TOTP_SESSION_MINUTES No 30 Minutes before TOTP re-authentication is required
TOTP_CHALLENGE_SECONDS No 120 Seconds the code entry window stays open
TOTP_LOCKOUT_ATTEMPTS No 3 Failed TOTP attempts before temporary lockout
TOTP_LOCKOUT_MINUTES No 15 TOTP lockout duration in minutes
FILE_RETENTION_DAYS No 0 Days to keep uploaded files before cleanup (0 to disable)

*Deprecated vars still work for backward compatibility when users.yaml is absent. When users.yaml is present, users.yaml wins and a warning is logged. Run make config to migrate.

CLAUDE_MAX_BUDGET_USD limits spending in a single session. For Claude Code, this maps to the --max-budget-usd CLI flag (on Pro/Max plans, purely a runaway prevention mechanism). Goose does not currently enforce this limit. The session resets on /new, model switch, or workspace switch.

Running

make run

Or manually: source .venv/bin/activate && python -m kai

Running as a service (macOS)

Create ~/Library/LaunchAgents/com.kai.bot.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.kai.bot</string>

    <key>ProgramArguments</key>
    <array>
        <string>/path/to/kai/.venv/bin/python</string>
        <string>-m</string>
        <string>kai</string>
    </array>

    <key>WorkingDirectory</key>
    <string>/path/to/kai</string>

    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
    </dict>

    <key>RunAtLoad</key>
    <true/>

    <key>KeepAlive</key>
    <true/>

    <key>ProcessType</key>
    <string>Background</string>
</dict>
</plist>

Replace /path/to/kai with your actual project path. The PATH must include directories for claude (or goose, depending on your backend), ffmpeg, and any other tools Kai shells out to.

launchctl load ~/Library/LaunchAgents/com.kai.bot.plist

Kai will start immediately and restart automatically on login or crash. Logs go to logs/kai.log with daily rotation (14 days of history). To stop:

launchctl unload ~/Library/LaunchAgents/com.kai.bot.plist

Running as a service (Linux)

Create /etc/systemd/system/kai.service:

[Unit]
Description=Kai Telegram Bot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=YOUR_USERNAME
WorkingDirectory=/path/to/kai
ExecStart=/path/to/kai/.venv/bin/python -m kai
Restart=always
RestartSec=5
Environment=PATH=/usr/local/bin:/usr/bin:/bin

[Install]
WantedBy=multi-user.target

Replace YOUR_USERNAME and /path/to/kai with your values. Add any extra directories to PATH where claude, ffmpeg, etc. are installed.

The network-online.target dependency ensures systemd waits for network connectivity before starting Kai, preventing DNS failures during boot.

sudo systemctl enable kai
sudo systemctl start kai

Check logs with tail -f logs/kai.log or journalctl -u kai -f. To stop:

sudo systemctl stop kai

Project Structure

kai/
ā”œā”€ā”€ src/kai/                  # Source package
│   ā”œā”€ā”€ __init__.py           # Version
│   ā”œā”€ā”€ __main__.py           # python -m kai entry point
│   ā”œā”€ā”€ backend.py            # Agent backend ABC and shared context injection
│   ā”œā”€ā”€ main.py               # Async startup and shutdown
│   ā”œā”€ā”€ bot.py                # Telegram handlers, commands, message routing
│   ā”œā”€ā”€ claude.py             # Persistent Claude Code subprocess management
│   ā”œā”€ā”€ config.py             # Environment and per-workspace config loading
│   ā”œā”€ā”€ goose.py              # Goose ACP backend (JSON-RPC subprocess)
│   ā”œā”€ā”€ sessions.py           # SQLite session, job, and settings storage
│   ā”œā”€ā”€ cron.py               # Scheduled job execution (APScheduler)
│   ā”œā”€ā”€ webhook.py            # HTTP server: GitHub/generic webhooks, scheduling API
│   ā”œā”€ā”€ history.py            # Conversation history (read/write JSONL logs)
│   ā”œā”€ā”€ pool.py               # Per-user agent subprocess pool (lazy creation, idle eviction)
│   ā”œā”€ā”€ locks.py              # Per-chat async locks and stop events
│   ā”œā”€ā”€ install.py            # Protected installation tooling
│   ā”œā”€ā”€ totp.py               # TOTP verification, rate limiting, and CLI
│   ā”œā”€ā”€ review.py             # PR review agent (one-shot agent subprocess)
│   ā”œā”€ā”€ triage.py             # Issue triage agent (one-shot agent subprocess)
│   ā”œā”€ā”€ services.py           # External service proxy for third-party APIs
│   ā”œā”€ā”€ transcribe.py         # Voice message transcription (ffmpeg + whisper-cpp)
│   ā”œā”€ā”€ tts.py                # Text-to-speech synthesis (Piper TTS + ffmpeg)
│   ā”œā”€ā”€ prompt_utils.py       # Shared prompt construction utilities
│   ā”œā”€ā”€ telegram_utils.py     # Telegram-specific helper functions
│   └── workspace_utils.py    # Workspace path resolution and validation
ā”œā”€ā”€ tests/                    # Test suite
ā”œā”€ā”€ home/                     # Agent home workspace
│   ā”œā”€ā”€ .claude/              # Identity and memory template
│   └── files/                # File exchange directory (created at runtime)
ā”œā”€ā”€ kai.db                    # SQLite database (gitignored, created at runtime)
ā”œā”€ā”€ logs/                     # Daily-rotated log files (gitignored)
ā”œā”€ā”€ models/                   # Whisper and Piper model files (gitignored)
ā”œā”€ā”€ services.yaml             # External service configs (gitignored)
ā”œā”€ā”€ workspaces.example.yaml   # Per-workspace config template
ā”œā”€ā”€ pyproject.toml            # Package metadata and dependencies
ā”œā”€ā”€ Makefile                  # Common dev commands
ā”œā”€ā”€ .env                      # Environment variables (gitignored, copy from .env.example)
└── LICENSE                   # Apache 2.0

Development

make setup      # Install in editable mode with dev tools
make lint       # Run ruff linter
make format     # Auto-format with ruff
make check      # Lint + format check (CI-friendly)
make test       # Run test suite
make run        # Start the bot

Production deployment

For a hardened installation that separates source, data, and secrets across protected directories:

python -m kai install config       # Interactive Q&A, writes install.conf (no sudo)
sudo python -m kai install apply   # Creates /opt layout, migrates data (root)
python -m kai install status       # Shows current installation state (no sudo)

This creates a split layout:

  • /opt/kai/ - read-only source and venv (root-owned)
  • /var/lib/kai/ - writable runtime data: database, logs, files (service-user-owned)
  • /etc/kai/ - secrets: env file, service configs, TOTP (root-owned, mode 0600)

The install module handles directory creation, source copying, venv setup, secret deployment, sudoers rules, data migration, service definition generation, and service lifecycle (stop/start). Use --dry-run to preview changes without applying them.

The service user reads secrets via narrowly-scoped sudoers rules (sudo cat on specific files only). The inner agent process cannot read secrets or modify source code.

License

Apache License 2.0. See LICENSE for details.

Release History

VersionChangesUrgencyDate
v1.4.0## What's Changed * Skip sudo wrapper when os_user matches bot process user by @dcellison in https://github.com/dcellison/kai/pull/192 * Generate users.yaml from config wizard instead of ALLOWED_USER_IDS by @dcellison in https://github.com/dcellison/kai/pull/204 * Extract self-sudo skip into shared resolve_claude_user() utility by @dcellison in https://github.com/dcellison/kai/pull/205 * Replace wall-clock limit with activity-based idle timeout by @dcellison in https://github.com/dcellison/kai/pHigh4/8/2026

Dependencies & License Audit

Loading dependencies...

Similar Packages

GENesis-AGIAutonomous AI agent with persistent memory, self-learning, and earned autonomy. Cognitive partner that remembers, learns, and evolves.v3.0a7
idle-harnessGAN-inspired multi-agent system that autonomously builds full-stack web apps from a single prompt using Claude AI agentsmain@2026-04-18
hermes-agentThe agent that grows with youv2026.4.16
career-opsAI-powered job search system built on Claude Code. 14 skill modes, Go dashboard, PDF generation, batch processing.v1.5.0
sawzhang_skillsClaude Code skills collection — CCA study guides, Twitter research, MCP review, auto-iteration tools0.0.0