- Notifications
You must be signed in to change notification settings - Fork 18
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.
When you switch workspaces, Kai:
- Kills the current Claude Code process
- Clears the session (resets cost tracking)
- Starts a fresh Claude process in the new directory on your next message
- 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.
/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).
/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.
/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.
/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/.
/workspaces
Shows an inline keyboard with your workspaces. Layout from top to bottom:
- Home - always first
- Pinned workspaces - from
ALLOWED_WORKSPACES, in config order - 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.
When working in a foreign workspace, Kai injects context at the start of each new session:
| Source | Injected when | Purpose |
|---|---|---|
Home AGENTS.md | Foreign workspace only | Core identity and instructions (the backend finds it natively at home) |
Per-user MEMORY.md | Memory disabled, private contexts only | Personal memory at DATA_DIR/memory/<principal_id>/MEMORY.md |
| Semantic memory hits | Memory enabled, private contexts | Facts retrieved per principal, scoped by the active memory project |
| Recent history | Always | Recent canonical conversation context for continuity |
| Scheduling API info | Always (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.
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.
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.
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
/workspaceskeyboard, 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.
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.
Copy the example file and customize:
cp templates/workspaces.yaml workspaces.yamlFor protected installations, the file goes at /etc/kai/workspaces.yaml instead.
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 | Type | Default | Description |
|---|---|---|---|
path | string | (required) | Workspace directory. Supports ~ expansion. Must exist. |
claude.model | string | registry default | Any curated model name across providers; other IDs load with a logged warning (see Validation) |
claude.timeout | int | global DEFAULT_TIMEOUT | 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.
pathmust be an existing directorymodelis 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
budgetkey from an older file is ignored with a logged warning; the entry still loads timeoutmust be a positive integer (floats like300.0are accepted if they're whole numbers;3.7is not)system_promptandsystem_prompt_fileare mutually exclusive - pick oneenv_fileandsystem_prompt_filemust 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.
- Protected installation:
/etc/kai/workspaces.yaml(checked first) - Development:
workspaces.yamlin the project root (fallback) - 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.
When a workspace has env and/or env_file configured, the variables are merged into the Claude subprocess environment in this order:
- Base environment (inherited from the parent process)
env_filevalues- Inline
envvalues (overrideenv_fileif both set the same key) - 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 ("").
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).
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.
- 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_WORKSPACESentries, deduplicated. - Confirmation messages: When switching to a configured workspace, the confirmation shows active overrides (e.g.,
(model: opus)).
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.
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]