Skip to content

Workspaces

Daniel Ellison edited this page Mar 17, 2026 · 4 revisions

Workspaces

Kai can work in any project on your machine, not just its home directory. The workspace system lets you switch between projects via Telegram, with memory and identity carrying over automatically.

How it works

When you switch workspaces, Kai:

  1. Kills the current Claude Code process
  2. Clears the session (resets cost tracking)
  3. Starts a fresh Claude process in the new directory on your next message
  4. Injects identity and memory from the home workspace so Kai stays "itself"

The workspace persists across restarts — if Kai is in a foreign workspace when the service restarts, it picks up where it left off.

Commands

Show current workspace

/workspace

Shows the current workspace path. If WORKSPACE_BASE is set, paths under it are shortened to relative names (e.g., backend instead of /Users/kai/Projects/backend).

Switch by name

/workspace backend
/workspace work/backend

Names are resolved relative to WORKSPACE_BASE (set in .env). Nested paths work — work/backend resolves to $WORKSPACE_BASE/work/backend.

Absolute paths and ~ are rejected for security. Names are resolved in order: WORKSPACE_BASE children first, then ALLOWED_WORKSPACES entries (matched by directory name). See Configuration for details.

Go home

/workspace home

Returns to Kai's default workspace (workspace/ in the project root). Always works, even without WORKSPACE_BASE set.

Create a new workspace

/workspace new my-project

Creates the directory under WORKSPACE_BASE, runs git init, and switches to it. Parent directories are created automatically — /workspace new clients/acme creates both clients/ and acme/.

Interactive picker

/workspaces

Shows an inline keyboard with your workspaces. Layout from top to bottom:

  1. Home - always first
  2. Pinned workspaces - from ALLOWED_WORKSPACES, in config order
  3. Recent history - deduplicated against pinned workspaces and home

The current workspace is marked with a green dot. Tap any workspace to switch, or tap the current one to dismiss.

If a workspace in your history no longer exists on disk, tapping it removes it from the list and refreshes the keyboard.

Memory injection

When working in a foreign workspace, Kai injects context at the start of each new session:

Source Injected when Purpose
Home CLAUDE.md Foreign workspace only Core identity and instructions
Home MEMORY.md Always Personal memory (preferences, people, ongoing context)
Workspace MEMORY.md Foreign workspace only Project-specific notes and conventions
Recent history Always Last few conversation exchanges for continuity
Scheduling API info Always (if configured) Enables job scheduling from any workspace

This means Kai knows who it is and remembers your preferences regardless of which project it's working in, while also picking up any project-specific context you've saved.

Foreign workspace safety

Working in foreign repos can trigger unintended behavior — Claude Code reads the project's CLAUDE.md, git branch names, and auto-memory, any of which might suggest autonomous work. To prevent this, Kai prepends a reminder to every message in foreign workspaces instructing Claude to respond only to what you wrote.

Configuration

WORKSPACE_BASE

Set this in your .env to enable workspace name resolution:

WORKSPACE_BASE=/Users/kai/Projects

Without it, /workspace home still works, but name-based switching (/workspace backend) and workspace creation (/workspace new) are disabled.

The directory must exist at startup - Kai validates it during config loading and exits with an error if it's missing.

ALLOWED_WORKSPACES

Comma-separated list of additional workspace paths that are accessible by name, even if they're outside WORKSPACE_BASE:

ALLOWED_WORKSPACES=/home/user/project-a,/home/user/project-b

This is useful for existing projects you want to access from Telegram without moving them under the base directory. Each entry:

  • Must be an absolute path to an existing directory
  • Is reachable via /workspace <dirname> (e.g., /workspace project-a)
  • Appears as a pinned entry in the /workspaces keyboard, above history
  • Does not trigger any initialization (no git init, no config files created) when you switch to it

Non-existent paths are skipped at startup with a warning rather than crashing, so a stale entry (e.g., an unmounted drive) won't block Kai from starting.

Name resolution order: When you type /workspace foo, Kai checks WORKSPACE_BASE children first, then ALLOWED_WORKSPACES entries. If both contain a directory named foo, the base directory wins.

Security: Absolute paths from Telegram are still rejected. This config is set on the server, not via Telegram commands - the operator controls which directories are accessible.

Per-workspace configuration

You can override Claude's model, budget, timeout, environment variables, and system prompt on a per-workspace basis using workspaces.yaml. This is purely declarative - no launcher scripts, no code changes. Workspaces without config entries use the global defaults from .env.

Setup

Copy the example file and customize:

cp workspaces.example.yaml workspaces.yaml

For protected installations, the file goes at /etc/kai/workspaces.yaml instead.

File format

workspaces:
  # Full config: Opus, high budget, long timeout, custom env, inline prompt
  - path: ~/projects/complex-app
    claude:
      model: opus
      budget: 20.0
      timeout: 300
      env:
        DATABASE_URL: "postgres://localhost/myapp"
      system_prompt: |
        This is a Django application. Run tests with pytest.

  # Env vars from an existing file (avoids dual-maintenance)
  - path: ~/projects/data-pipeline
    claude:
      model: sonnet
      env_file: ~/projects/data-pipeline/.env.kai

  # Env file as a base with inline overrides on top
  - path: ~/projects/staging
    claude:
      env_file: ~/projects/staging/.env
      env:
        RAILS_ENV: "staging"

  # Lightweight docs workspace with Haiku
  - path: ~/projects/docs
    claude:
      model: haiku

  # System prompt from a file (re-read each session, so edits apply without restart)
  - path: ~/projects/firmware
    claude:
      system_prompt_file: /etc/kai/prompts/firmware.txt

  # Minimal: just makes the workspace accessible, uses all global defaults
  - path: ~/projects/notes

Field reference

Field Type Default Description
path string (required) Workspace directory. Supports ~ expansion. Must exist.
claude.model string global CLAUDE_MODEL haiku, sonnet, or opus
claude.budget float global CLAUDE_MAX_BUDGET_USD USD per-session spending cap. Must be > 0.
claude.timeout int global CLAUDE_TIMEOUT_SECONDS Seconds per readline. Must be > 0 (integer only).
claude.env dict {} Inline environment variables. Values are coerced to strings.
claude.env_file string none Path to a KEY=VALUE file. Must exist at load time. Re-read each session.
claude.system_prompt string none Inline system prompt text. Injected as ## Workspace Instructions.
claude.system_prompt_file string none Path to a prompt file. Must exist at load time. Re-read each session.

The entire claude: section is optional. Entries with only path are valid - they make the workspace accessible (added to the allowed set) while using global defaults.

Validation

  • path must be an existing directory
  • model must be one of haiku, sonnet, opus
  • budget must be a positive number (booleans are explicitly rejected to prevent float(True) silently becoming 1.0)
  • timeout must be a positive integer (floats like 300.0 are accepted if they're whole numbers; 3.7 is not)
  • system_prompt and system_prompt_file are mutually exclusive - pick one
  • env_file and system_prompt_file must point to existing, readable files
  • Duplicate paths: the first entry wins, with a warning logged
  • Non-existent paths: the entry is skipped with a warning

Invalid entries are skipped individually - one bad entry doesn't block the rest from loading.

Config loading priority

  1. Protected installation: /etc/kai/workspaces.yaml (checked first)
  2. Development: workspaces.yaml in the project root (fallback)
  3. Missing: everything uses global defaults from .env

If the protected file exists but is malformed YAML, loading stops entirely - it does not fall through to the local file. This is intentional: a broken protected config should be fixed, not silently bypassed.

Environment variable merge order

When a workspace has env and/or env_file configured, the variables are merged into the Claude subprocess environment in this order:

  1. Base environment (inherited from the parent process)
  2. env_file values
  3. Inline env values (override env_file if both set the same key)
  4. Webhook secret (set last - this is a security invariant; workspace config cannot override it)

YAML coercion notes: YAML parses bare true/false as booleans and ~ or empty values as null. The loader handles this: booleans become lowercase strings ("true", "false"), and nulls become empty strings ("").

System prompt injection

When a workspace has a system_prompt or system_prompt_file, the content is injected at the start of the first message in each new session, under a ## Workspace Instructions heading. It sits between identity/memory context and the conversation history.

File-based prompts (system_prompt_file) are re-read each session, so you can edit the file without restarting Kai. If the file is deleted after initial validation, the session proceeds without it (a warning is logged).

Reset-then-override on workspace switch

When you switch workspaces, Kai always resets the model, budget, and timeout to their global defaults first, then applies the new workspace's overrides (if any). This prevents config from leaking between workspaces.

For example: if workspace A sets budget: 15.0 and workspace B only sets model: haiku, switching from A to B correctly resets the budget to the global default rather than carrying over A's $15.

Interaction with other features

  • ALLOWED_WORKSPACES: Workspaces defined in YAML are automatically added to the allowed set. You don't need to list them in both places.
  • /model command: Overrides the model for the current session. The workspace config (or global default) reapplies on the next workspace switch.
  • /workspaces keyboard: YAML-defined workspaces appear alongside ALLOWED_WORKSPACES entries, deduplicated.
  • Confirmation messages: When switching to a configured workspace, the confirmation shows active overrides (e.g., (model: opus, budget: $15.00)).

History and persistence

Workspace switches are recorded in SQLite with timestamps. The /workspaces keyboard shows up to 10 recent workspaces, ordered by most recently used.

The current workspace is persisted as a setting in the database. On restart, Kai reads this setting and restores the workspace. Returning to the home workspace deletes the setting — home is the default state, not an explicit choice that needs saving.

Example workflow

You: /workspace new api-server
Kai: Created and switched to /Users/kai/Projects/api-server (git initialized). Session cleared.

You: Set up a basic Express server with TypeScript
Kai: [works in api-server directory with full tool access]

You: /workspace home
Kai: Switched to home workspace. Session cleared.

You: What did we set up in the api-server project?
Kai: [remembers via conversation history and home memory]

You: /workspaces
Kai: [shows keyboard: 🟢 Home, api-server]

Clone this wiki locally