-
Notifications
You must be signed in to change notification settings - Fork 2
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.
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.
/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_WORKSPACES entries (matched by directory name). See Configuration for details.
/workspace home
Returns to Kai's default workspace (workspace/ in the project root). 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 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.
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, 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.
Copy the example file and customize:
cp workspaces.example.yaml workspaces.yamlFor protected installations, the file goes at /etc/kai/workspaces.yaml instead.
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 | 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.
-
pathmust be an existing directory -
modelmust be one ofhaiku,sonnet,opus -
budgetmust be a positive number (booleans are explicitly rejected to preventfloat(True)silently becoming1.0) -
timeoutmust be a positive integer (floats like300.0are accepted if they're whole numbers;3.7is not) -
system_promptandsystem_prompt_fileare mutually exclusive - pick one -
env_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, 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.
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, 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.
- 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, budget: $15.00)).
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]