Skip to content

Protected Installation

Daniel Ellison edited this page Mar 26, 2026 · 5 revisions

Protected Installation

A protected installation deploys Kai with hardened directory permissions, separating read-only source code, writable runtime data, and root-owned secrets across three locations. This is the recommended setup for always-on deployments where the bot runs as a system service.

Why use a protected installation?

In development mode (make run), everything lives under the project directory and runs as your user. That's fine for hacking, but for a persistent service you want stronger isolation:

  • Source code is read-only - owned by root, the service user can't modify it
  • Secrets are root-owned - .env equivalent lives at /etc/kai/env with mode 0600; the service user reads it via targeted sudoers rules
  • Runtime data is separated - database, logs, and file exchange live in their own directory
  • The inner Claude process can't access secrets - it runs as the service user (or optionally a separate user) with no access to /etc/kai/

The installer handles both macOS (launchd) and Linux (systemd).

Prerequisites

  • Python 3.13+ on the host
  • Claude Code CLI installed for the service user (~/.local/bin/claude)
  • A Telegram bot token and your user ID (see Getting Started)
  • A dedicated OS user for the service (e.g., kai) - optional but recommended
  • macOS or Linux - the installer detects and handles both

The two-step workflow

The installer uses a two-step process that separates privilege levels:

python -m kai install config     # no sudo - interactive Q&A, writes install.conf
sudo python -m kai install apply  # root - reads install.conf, creates the layout
python -m kai install status      # no sudo - reports current state

install.conf bridges the two steps: config writes it, apply reads it. It's a JSON file stored in the project root with restricted permissions (mode 0600) since it contains secrets.

Step 1: Configure

Run from the project directory as your normal user:

python -m kai install config

This walks through an interactive Q&A. If install.conf already exists, its values are used as defaults so re-running only asks about changes.

Prompted fields

Installation paths:

Field Default Description
Install location /opt/kai Where source code and venv go (root-owned)
Data directory /var/lib/kai Where database, logs, and files go (service-user-owned)
Service user kai The OS user the service runs as
Platform auto-detected darwin or linux

Telegram:

Field Default Description
Bot token (required) From @BotFather
Allowed user IDs (required*) Comma-separated Telegram user IDs (*not required when users.yaml exists)
Transport polling polling or webhook
Webhook URL Required if transport is webhook
Webhook secret Optional Telegram webhook verification secret

Claude:

Field Default Description
Model sonnet haiku, sonnet, or opus
Timeout 120 Seconds before Claude times out
Budget 10.0 Max USD per session

Webhook server:

Field Default Description
Port 8080 Webhook/API server port
Secret auto-generated Required for all /api/* and /webhook/* endpoints

Workspaces:

Field Default Description
Workspace base Base directory for workspace name resolution (e.g., ~/Projects)
Allowed workspaces Comma-separated absolute paths for pinned workspaces

Optional features:

Field Default Description
Voice transcription false Enable whisper.cpp STT
Text-to-speech false Enable Piper TTS
Claude subprocess user Optional separate OS user for process isolation (see User separation below)
Perplexity API key For the external services proxy

After answering all prompts, the config is written to install.conf:

Configuration written to /path/to/kai/install.conf
Review the file, then run: sudo python -m kai install apply

Step 2: Apply

Run as root:

sudo python -m kai install apply

This reads install.conf and performs the installation in order:

  1. Stops the service if it's already running
  2. Creates directories with correct ownership and permissions (see Directory layout)
  3. Copies source to the install location (clean copy, excludes __pycache__, .pyc, .git, .venv, .env)
  4. Creates or updates the venv with Python 3.13+; on update, only rebuilds if pyproject.toml changed (checksum comparison)
  5. Copies model files (whisper, Piper) if present in the source directory
  6. Writes secrets to /etc/kai/env (mode 0600) and copies services.yaml to /etc/kai/ if present
  7. Configures sudoers at /etc/sudoers.d/kai (validated with visudo -cf before writing)
  8. Migrates runtime data from dev layout if this is the first protected install (database and logs; never overwrites existing files)
  9. Generates and installs the service definition (launchd plist or systemd unit)
  10. Starts the service

Dry run

Preview everything without making changes:

sudo python -m kai install apply --dry-run

Every action is printed with a [DRY RUN] prefix. Always use this on your first attempt.

Step 3: Verify

Run as your normal user:

python -m kai install status

This reports the current installation state without requiring sudo:

Kai Installation Status
==============================
Installation: /opt/kai (exists, root:root)
Data: /var/lib/kai (exists, kai:kai)
Secrets: /etc/kai/env (exists, root:root)
Services: /etc/kai/services.yaml (exists, root:root)
Sudoers: /etc/sudoers.d/kai (exists, root:root)
Service: com.syrinx.kai (loaded)
Version: 0.1.0

It also checks workspace path traversal - if the service user can't traverse any parent directory of a configured workspace, you'll see a warning with the specific fix (e.g., chmod o+x /Users).

Directory layout

A protected installation creates this structure:

/opt/kai/                root:root  755   Read-only install tree
  src/                   root:root  755   Python source
  venv/                  root:root  755   Virtual environment
  home/                  kai:kai    755   Inner Claude home workspace
  pyproject.toml         root:root  644   Package metadata
  run.sh                 root:root  755   Launcher script (macOS only)
  models/                root:root  755   Whisper/Piper models (if present)
  .pyproject.sha256      root:root  644   Venv update detection checksum

/var/lib/kai/            kai:kai    755   Runtime data
  kai.db                 kai:kai    644   SQLite database
  logs/                  kai:kai    755   Log files (daily rotation)
  files/                 kai:kai    755   File exchange directory
    <chat_id>/           kai:kai    755   Per-user upload subdirectories
  history/               kai:kai    755   Conversation history
    <chat_id>/           kai:kai    755   Per-user history (JSONL, one file per day)
  memory/                kai:kai    755   Persistent memory
    MEMORY.md            kai:kai    644   Home memory file
  .responding/           kai:kai    755   Crash recovery flags (per-user)

/etc/kai/                root:root  755   Secrets
  env                    root:root  600   Environment variables
  services.yaml          root:root  600   External service API keys
  users.yaml             root:root  600   Per-user config (if present)
  workspaces.yaml        root:root  600   Per-workspace config (if present)
  totp.secret            root:root  600   TOTP secret (if configured)
  totp.attempts          root:root  600   TOTP attempt tracking

The key principle: the service user (kai) can write to /var/lib/kai/ and /opt/kai/home/, but cannot read /etc/kai/env directly. At startup, config.py reads secrets via sudo cat /etc/kai/env using the NOPASSWD sudoers rules the installer configured.

Runtime data (history, memory, files) lives entirely in /var/lib/kai/, never inside the install tree. This means make install / install apply can cleanly re-copy source without destroying conversation history or memory.

User separation (CLAUDE_USER)

By default, both the bot process and the inner Claude Code process run as the same service user. Process isolation adds a second layer: the inner Claude process runs as a different OS user entirely.

There are two ways to configure this:

  • CLAUDE_USER (env var) - global setting, applies to all subprocesses. Best for single-user deployments.
  • os_user (per-user in users.yaml) - each user's subprocess runs as their own OS account. Best for multi-user deployments. Takes precedence over CLAUDE_USER.

See Multi-User Setup for the full multi-user configuration.

What it does

When process isolation is configured:

  • The bot starts Claude with sudo -u <os_user> claude ... instead of just claude ...
  • Claude runs in a new process session (start_new_session=True) so signals from the bot don't leak to it
  • The sudoers rules include rules allowing the service user to run the claude binary as each configured OS user

What Claude can and cannot access

Can access Cannot access
Its own home directory /etc/kai/ (secrets)
The workspace directory Kai's database (/var/lib/kai/kai.db)
Tools on PATH Kai's source code (root-owned)

Setup

  1. Create the OS user(s) (e.g., claude, or per-user accounts like alice, bob)
  2. For single-user: set CLAUDE_USER=claude during install config. For multi-user: set os_user per user in users.yaml.
  3. Run sudo install apply - it generates the sudoers rules automatically
  4. Make sure the Claude Code CLI is installed and accessible to each OS user (the sudoers rules reference its path)

This is optional. For single-user local deployments, the default (no isolation) is fine. User separation is most valuable when you want defense in depth - even if Claude is compromised, it can't read bot secrets or modify the database. In multi-user deployments, it also prevents one user's subprocess from accessing another user's files.

Updating an existing installation

Re-run the same two steps:

  1. python -m kai install config - only if config values changed (existing values are shown as defaults)
  2. sudo python -m kai install apply - handles updates automatically:
    • Source is re-copied (clean copy every time)
    • Venv is only rebuilt if pyproject.toml changed
    • Secrets are re-written
    • Service is stopped before changes and started after
    • Database is never wiped on update

macOS vs Linux

Aspect macOS Linux
Service type LaunchDaemon plist systemd unit
Service location /Library/LaunchDaemons/com.syrinx.kai.plist /etc/systemd/system/kai.service
Launcher script Yes (run.sh) - needed because Homebrew Python re-execs through Python.app, changing the PID No - systemd tracks the process directly
Start/stop launchctl bootstrap/bootout system/com.syrinx.kai systemctl start/stop kai
Auto-restart KeepAlive in plist Restart=always in unit
Boot start RunAtLoad in plist WantedBy=multi-user.target
Directory layout Same Same
Sudoers rules Same (binary paths auto-detected) Same

The launcher script on macOS deserves a note: Homebrew Python's framework binary fork-execs through Python.app, creating a grandchild process with a new PID. Launchd loses track of this. The run.sh wrapper stays as the parent process launchd tracks and forwards SIGTERM to the real Python process for graceful shutdown.

Troubleshooting

Service won't start Check kai.log in the data directory (/var/lib/kai/logs/). Common causes: missing claude binary on PATH, Python version mismatch, or permission errors on the workspace directory.

"Permission denied" reading /etc/kai/env The sudoers rules may be missing or incorrect. Verify with sudo visudo -cf /etc/sudoers.d/kai. Re-run sudo python -m kai install apply to regenerate them.

Claude binary not found The claude CLI must be installed under the service user's ~/.local/bin/. The generated plist/unit includes this in PATH, but if you manually edited the service definition, check the PATH. The sudoers rule for CLAUDE_USER also references the binary path - re-run install apply if you reinstalled Claude Code.

"Python >= 3.13 required" during apply The installer requires Python 3.13+. Install it and ensure it's the default python3 on your PATH, or install as python3.13 (the installer checks both).

Database not found after migration The installer copies kai.db from the project root to /var/lib/kai/ on first apply. Existing files are never overwritten. If you see database errors, check that KAI_DATA_DIR is set correctly in the service environment.

Workspace traversal warnings The installer and status command check that the service user can traverse every parent directory of configured workspace paths. If you see a warning like WARNING: /Users lacks execute permission for kai, follow the suggested fix (e.g., chmod o+x /Users).

Always dry-run first Use sudo python -m kai install apply --dry-run to preview all changes before applying.

Clone this wiki locally