Skip to content

Workspaces

Daniel Ellison edited this page Sep 3, 2026 · 1 revision

Workspaces

Kai can work in any project on your machine, not just its home directory. The workspace system lets you switch between projects from either surface, with memory and identity carrying over automatically: in the Workshop, use the workspace dropdown in the channel context pane (disabled while a run is active); on Telegram, use the /workspace commands this page documents.

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.

On canonical runtimes the switch runs through the settings workspace authority, which brings a few behaviors worth knowing: switches are revisioned (a concurrent change from another surface conflicts instead of silently losing), a switch is refused while a run is executing (the Workshop dropdown greys out; Telegram reports busy), access is checked against your allowed set, and a failed switch restores the previous workspace rather than leaving you stranded in between.

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-workspace entries matched by directory name; the allowed set is the union of the global ALLOWED_WORKSPACES, your per-user allowed_workspaces list in users.yaml, and paths you added at runtime with /workspace allow. See Configuration for details.

Go home

/workspace home

Returns to Kai's default workspace (home/ in the project root, or home_workspace from users.yaml). 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:

SourceInjected whenPurpose
Home AGENTS.mdForeign workspace onlyCore identity and instructions (the backend finds it natively at home)
Per-user MEMORY.mdMemory disabled, private contexts onlyPersonal memory at DATA_DIR/memory/<principal_id>/MEMORY.md
Semantic memory hitsMemory enabled, private contextsFacts retrieved per principal, scoped by the active memory project
Recent historyAlwaysRecent canonical conversation context for continuity
Scheduling API infoAlways (if configured)Enables job scheduling from any workspace

The workspace itself also determines memory scope: memory-projects.yaml maps directories to memory projects, deciding which project-scoped memories can surface while you work there. It answers a different question than workspaces.yaml (which configures how the backend runs). /workspace new registers its directory as a memory project automatically, and /project register manages entries at runtime; see Memory.

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, 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 templates/workspaces.yaml workspaces.yaml

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

File format

workspaces:
# Full config: Opus, long timeout, custom env, inline prompt
- path: ~/projects/complex-appclaude:
model: opustimeout: 300env:
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-pipelineclaude:
model: sonnetenv_file: ~/projects/data-pipeline/.env.kai# Env file as a base with inline overrides on top
- path: ~/projects/stagingclaude:
env_file: ~/projects/staging/.envenv:
RAILS_ENV: "staging"# Lightweight docs workspace with Haiku
- path: ~/projects/docsclaude:
model: haiku# System prompt from a file (re-read each session, so edits apply without restart)
- path: ~/projects/firmwareclaude:
system_prompt_file: /etc/kai/prompts/firmware.txt# Minimal: just makes the workspace accessible, uses all global defaults
- path: ~/projects/notes

Field reference

FieldTypeDefaultDescription
pathstring(required)Workspace directory. Supports ~ expansion. Must exist.
claude.modelstringregistry defaultAny curated model name across providers; other IDs load with a logged warning (see Validation)
claude.timeoutintglobal DEFAULT_TIMEOUTSeconds per readline. Must be > 0 (integer only).
claude.envdict{}Inline environment variables. Values are coerced to strings.
claude.env_filestringnonePath to a KEY=VALUE file. Must exist at load time. Re-read each session.
claude.system_promptstringnoneInline system prompt text. Injected as ## Workspace Instructions.
claude.system_prompt_filestringnonePath 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 is checked against the curated set across all providers; a name outside it logs a warning but the entry still loads, so open-ended provider IDs work. At switch time an override is applied only when valid for the active user's backend; a mismatch is skipped with a warning
  • a lingering budget key from an older file is ignored with a logged warning; the entry still loads
  • 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, the loader logs a warning and continues with no workspace overrides at all; it does not fall through to the local file. Watch the startup log for the warning: a broken protected config costs you every workspace override, with only that log line to show for it.

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 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 timeout: 300 and workspace B only sets model: haiku, switching from A to B correctly resets the timeout to the global default rather than carrying over A's 300 seconds.

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)).

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