-
Notifications
You must be signed in to change notification settings - Fork 4.4k
04.4 Workspace Management
Relevant source files
The following files were used as context for generating this wiki page:
This document explains how ZeroClaw manages workspace directories, configuration files, and identity assets. The workspace is the root directory where ZeroClaw stores its configuration, identity files, memory databases, and agent state. Understanding workspace management is essential for multi-profile deployments, security configuration, and identity customization.
For provider configuration, see Built-in Providers. For secret encryption within the workspace, see Secret Management. For memory storage within the workspace, see Memory System.
ZeroClaw uses a hierarchical directory structure rooted at ~/.zeroclaw by default. The workspace contains all configuration, state, identity files, and persistent storage.
Default Directory Layout:
~/.zeroclaw/
├── config.toml # Main configuration file
├── active_workspace.toml # Optional: marks active workspace for multi-profile
├── .secret_key # Encryption key for secrets (when secrets.encrypt = true)
├── auth-profiles.json # OAuth profiles (OpenAI Codex, Claude Code)
├── workspace/ # Agent workspace root
│ ├── IDENTITY.md # Who the agent is
│ ├── SOUL.md # Core personality and values
│ ├── USER.md # Who the agent is helping
│ ├── AGENTS.md # Behavior guidelines
│ ├── TOOLS.md # Tool preferences (optional)
│ ├── BOOTSTRAP.md # First-run ritual (optional)
│ ├── MEMORY.md # Curated long-term memory
│ ├── skills/ # Skill manifests and instructions
│ │ └── example-skill/
│ │ ├── SKILL.toml # Skill metadata
│ │ └── SKILL.md # Skill instructions
│ ├── memory/ # SQLite memory databases
│ │ └── memories.db
│ └── state/ # Runtime state
│ ├── models_cache.json # Provider model cache
│ ├── cron_jobs.json # Scheduled jobs
│ └── heartbeat.json # Heartbeat state
Key Files:
| File | Purpose | Created By |
|---|---|---|
config.toml |
Main configuration | Config::save() |
active_workspace.toml |
Workspace selection marker | persist_workspace_selection() |
.secret_key |
ChaCha20Poly1305 encryption key | SecretStore::init() |
auth-profiles.json |
OAuth profiles (encrypted) |
auth login command |
workspace/IDENTITY.md |
Agent identity | scaffold_workspace() |
workspace/memory/memories.db |
SQLite memory backend | SqliteMemory::new() |
Sources: src/config/schema.rs:48-144, README.md:493-599
ZeroClaw resolves the workspace directory using a three-tier resolution strategy with explicit environment variable override support.
flowchart TB
Start["Program Start"] --> CheckEnv{"ZEROCLAW_WORKSPACE<br/>env var set?"}
CheckEnv -->|Yes| ValidateEnv["Validate path exists"]
CheckEnv -->|No| CheckMarker{"active_workspace.toml<br/>exists?"}
ValidateEnv --> EnvValid{"Path valid?"}
EnvValid -->|Yes| UseEnv["Use ZEROCLAW_WORKSPACE"]
EnvValid -->|No| Error["Error: Invalid workspace path"]
CheckMarker -->|Yes| ReadMarker["Read workspace_path from TOML"]
CheckMarker -->|No| UseDefault["Use ~/.zeroclaw/workspace"]
ReadMarker --> MarkerValid{"Path valid?"}
MarkerValid -->|Yes| UseMarker["Use marked workspace"]
MarkerValid -->|No| UseDefault
UseEnv --> LoadConfig["Load config.toml from workspace parent"]
UseMarker --> LoadConfig
UseDefault --> LoadConfig
LoadConfig --> InitSubsystems["Initialize subsystems with workspace_dir"]
Error --> Exit["Exit with error"]
Resolution Priority:
-
ZEROCLAW_WORKSPACEenvironment variable (highest) -
active_workspace.tomlmarker file -
~/.zeroclaw/workspacedefault path (lowest)
Code Implementation:
The resolve_workspace_dir() function in src/config/schema.rs:1317-1352 implements this resolution logic:
// Pseudocode representation
fn resolve_workspace_dir() -> Result<PathBuf> {
// 1. Check ZEROCLAW_WORKSPACE env var
if let Ok(workspace_env) = std::env::var("ZEROCLAW_WORKSPACE") {
let path = shellexpand::tilde(&workspace_env).into_owned();
return Ok(PathBuf::from(path));
}
// 2. Check active_workspace.toml marker
let zeroclaw_dir = UserDirs::new()?.home_dir().join(".zeroclaw");
let marker_path = zeroclaw_dir.join("active_workspace.toml");
if marker_path.exists() {
let content = fs::read_to_string(&marker_path)?;
let marker: ActiveWorkspaceMarker = toml::from_str(&content)?;
return Ok(PathBuf::from(marker.workspace_path));
}
// 3. Default to ~/.zeroclaw/workspace
Ok(zeroclaw_dir.join("workspace"))
}Sources: src/config/schema.rs:1317-1352
ZeroClaw supports multiple workspace profiles through the active_workspace.toml marker file. This enables switching between different agent identities, configurations, and memory stores without manual path manipulation.
The marker file is a simple TOML document:
workspace_path = "/path/to/custom/workspace"Creation:
The persist_workspace_selection() function in src/config/schema.rs:1354-1382 creates the marker:
fn persist_workspace_selection(config_path: &Path) -> Result<()> {
let workspace_dir = config_path.parent()
.context("Config path has no parent")?
.join("workspace");
let zeroclaw_dir = UserDirs::new()?.home_dir().join(".zeroclaw");
let marker_path = zeroclaw_dir.join("active_workspace.toml");
let marker = ActiveWorkspaceMarker {
workspace_path: workspace_dir.display().to_string(),
};
fs::write(&marker_path, toml::to_string(&marker)?)?;
Ok(())
}flowchart LR
User["User"] --> Command1["zeroclaw onboard"]
Command1 --> Profile1["~/.zeroclaw/workspace"]
Profile1 --> Marker1["active_workspace.toml → workspace"]
User --> Command2["ZEROCLAW_WORKSPACE=~/work/agent zeroclaw onboard"]
Command2 --> Profile2["~/work/agent/workspace"]
Profile2 --> Marker2["active_workspace.toml → work/agent/workspace"]
Marker1 -.switch.-> Agent1["Agent uses Profile 1"]
Marker2 -.switch.-> Agent2["Agent uses Profile 2"]
User --> Switch["Edit active_workspace.toml"]
Switch --> Marker1
Switch --> Marker2
Use Cases:
| Scenario | Solution |
|---|---|
| Personal vs. work agent | Two workspace profiles with different identities |
| Development vs. production | Separate configs and memory stores |
| Multi-tenant deployment | One workspace per customer/project |
| Testing isolation | Ephemeral workspace for integration tests |
Sources: src/config/schema.rs:1354-1382, src/onboard/wizard.rs:158
ZeroClaw constructs its system prompt by loading markdown files from the workspace directory. These files define the agent's identity, behavior, and capabilities.
The load_openclaw_bootstrap_files() function in src/channels/mod.rs:846-870 loads identity files:
fn load_openclaw_bootstrap_files(
prompt: &mut String,
workspace_dir: &Path,
max_chars_per_file: usize,
) {
let bootstrap_files = [
"AGENTS.md",
"SOUL.md",
"TOOLS.md",
"IDENTITY.md",
"USER.md"
];
for filename in &bootstrap_files {
inject_workspace_file(prompt, workspace_dir, filename, max_chars_per_file);
}
// Optional files
if workspace_dir.join("BOOTSTRAP.md").exists() {
inject_workspace_file(prompt, workspace_dir, "BOOTSTRAP.md", max_chars_per_file);
}
inject_workspace_file(prompt, workspace_dir, "MEMORY.md", max_chars_per_file);
}flowchart TD
Start["build_system_prompt()"] --> ToolSection["1. Tools<br/>Tool descriptions + usage protocol"]
ToolSection --> HardwareSection["1b. Hardware<br/>GPIO/Arduino instructions if enabled"]
HardwareSection --> TaskSection["1c. Task Instructions<br/>Action-oriented guidance"]
TaskSection --> SafetySection["2. Safety<br/>Guardrails + best practices"]
SafetySection --> SkillsSection["3. Skills<br/>On-demand skill manifest"]
SkillsSection --> WorkspaceSection["4. Workspace<br/>Working directory path"]
WorkspaceSection --> IdentityCheck{"identity.format?"}
IdentityCheck -->|"aieos"| LoadAIEOS["Load AIEOS JSON<br/>aieos_path or aieos_inline"]
IdentityCheck -->|"openclaw"| LoadOpenClaw["Load Bootstrap Files<br/>AGENTS.md, SOUL.md, etc."]
LoadAIEOS --> DateSection["6. Date & Time<br/>Timezone for cache stability"]
LoadOpenClaw --> DateSection
DateSection --> RuntimeSection["7. Runtime<br/>Host, OS, model name"]
RuntimeSection --> Return["Return complete system prompt"]
| File | Purpose | Loaded By | Max Chars |
|---|---|---|---|
AGENTS.md |
Behavior guidelines | load_openclaw_bootstrap_files() |
20,000 |
SOUL.md |
Core values and personality | load_openclaw_bootstrap_files() |
20,000 |
TOOLS.md |
Tool preferences (optional) | load_openclaw_bootstrap_files() |
20,000 |
IDENTITY.md |
Agent identity and name | load_openclaw_bootstrap_files() |
20,000 |
USER.md |
User profile and preferences | load_openclaw_bootstrap_files() |
20,000 |
BOOTSTRAP.md |
First-run ritual (optional) | load_openclaw_bootstrap_files() |
20,000 |
MEMORY.md |
Curated long-term memory | load_openclaw_bootstrap_files() |
20,000 |
Character Limit Constant:
const BOOTSTRAP_MAX_CHARS: usize = 20_000;Defined in src/channels/mod.rs:59.
File Injection:
Each file is truncated to BOOTSTRAP_MAX_CHARS to prevent context window overflow. Files are injected with markers:
---IDENTITY.md---
[file content here]
---Sources: src/channels/mod.rs:846-870, src/channels/mod.rs:872-1043, README.md:662-747
The Config struct stores all system settings and is serialized to config.toml using the TOML format.
flowchart TD
LoadOrInit["Config::load_or_init()"] --> ResolveWS["resolve_workspace_dir()"]
ResolveWS --> ConfigPath["config_path = workspace_dir.parent()/config.toml"]
ConfigPath --> FileExists{"config.toml exists?"}
FileExists -->|No| CreateDefault["Config::default()<br/>with workspace_dir"]
FileExists -->|Yes| ReadFile["fs::read_to_string(config_path)"]
CreateDefault --> SaveNew["config.save()"]
SaveNew --> Return["Return Config"]
ReadFile --> ParseTOML["toml::from_str()"]
ParseTOML --> CheckEncrypt{"secrets.encrypt?"}
CheckEncrypt -->|Yes| DecryptSecrets["SecretStore::decrypt()<br/>ChaCha20Poly1305"]
CheckEncrypt -->|No| ApplyEnv["Apply env var overrides"]
DecryptSecrets --> ApplyEnv
ApplyEnv --> Validate["Validate config<br/>(proxy, tunnel, etc.)"]
Validate --> Return
Configuration values can be overridden at runtime using environment variables. This follows the three-tier priority model:
Priority Order:
- Environment variables (highest)
-
config.tomlvalues - Built-in defaults (lowest)
Common Overrides:
| Environment Variable | Config Field | Example |
|---|---|---|
OPENROUTER_API_KEY |
api_key |
sk-or-v1-... |
ANTHROPIC_API_KEY |
api_key |
sk-ant-... |
ZEROCLAW_WORKSPACE |
workspace_dir |
/custom/path |
OLLAMA_API_KEY |
api_key |
ollama_key |
Code Implementation:
Environment variable application happens in Config::load_or_init() after TOML parsing:
// Pseudocode from src/config/schema.rs:1400-1432
fn load_or_init() -> Result<Config> {
let mut config = parse_toml_or_default()?;
// Override api_key from env vars
if let Ok(key) = std::env::var("OPENROUTER_API_KEY") {
config.api_key = Some(key);
} else if let Ok(key) = std::env::var("ANTHROPIC_API_KEY") {
config.api_key = Some(key);
}
// ... additional overrides
Ok(config)
}Sources: src/config/schema.rs:1354-1432, README.md:492-599
Workspace scoping is a core security feature that restricts file system operations to the workspace directory tree. This prevents accidental or malicious access to sensitive system directories.
[autonomy]
level = "supervised" # "readonly", "supervised", "full"
workspace_only = true # Enforce workspace scoping
forbidden_paths = [
"/etc", "/root", "/proc", "/sys",
"~/.ssh", "~/.gnupg", "~/.aws"
]The security model enforces workspace scoping at multiple layers:
- Path Validation: All file operations validate paths before execution
- Symlink Detection: Symlinks are canonicalized and checked
- Forbidden Directories: 14 system directories are hardcoded as blocked
- Null Byte Injection: Null bytes in paths are rejected
flowchart TD
FileOp["File Operation Request<br/>(read/write/execute)"] --> CheckAutonomy{"autonomy.level?"}
CheckAutonomy -->|"readonly"| DenyWrite["Reject all writes"]
CheckAutonomy -->|"supervised"| CheckApproval["Require user approval"]
CheckAutonomy -->|"full"| PathChecks["Continue to path validation"]
DenyWrite --> Error["Return error"]
CheckApproval --> Approved{"User approves?"}
Approved -->|No| Error
Approved -->|Yes| PathChecks
PathChecks --> CheckNull{"Path contains<br/>null byte?"}
CheckNull -->|Yes| Error
CheckNull -->|No| CheckSymlink["Canonicalize path<br/>(resolve symlinks)"]
CheckSymlink --> CheckForbidden{"Path in<br/>forbidden_paths?"}
CheckForbidden -->|Yes| Error
CheckForbidden -->|No| CheckWorkspace{"workspace_only<br/>enabled?"}
CheckWorkspace -->|No| Execute["Execute operation"]
CheckWorkspace -->|Yes| InWorkspace{"Resolved path<br/>starts with workspace_dir?"}
InWorkspace -->|No| Error
InWorkspace -->|Yes| Execute
Hardcoded Forbidden Paths:
| Path | Reason |
|---|---|
/etc |
System configuration |
/root |
Root user home |
/proc |
Process information |
/sys |
Kernel interface |
/dev |
Device files |
/boot |
Boot loader |
/var/log |
System logs |
/usr/bin |
System binaries |
~/.ssh |
SSH keys |
~/.gnupg |
GPG keys |
~/.aws |
AWS credentials |
~/.kube |
Kubernetes config |
These paths are blocked regardless of workspace_only setting.
Sources: README.md:385-404, src/config/schema.rs:66-68
The onboarding wizard creates and scaffolds the workspace with identity files and directory structure.
sequenceDiagram
participant User
participant Wizard as "run_wizard()"
participant Setup as "setup_workspace()"
participant Scaffold as "scaffold_workspace()"
participant Config as "Config::save()"
participant Marker as "persist_workspace_selection()"
User->>Wizard: zeroclaw onboard --interactive
Wizard->>Setup: Step 1: Workspace Setup
Setup->>Setup: Resolve ~/.zeroclaw/workspace
Setup->>Setup: fs::create_dir_all(workspace_dir)
Setup-->>Wizard: (workspace_dir, config_path)
Wizard->>Wizard: Steps 2-7: Provider, Channels, etc.
Wizard->>Wizard: Step 8: Project Context
Wizard->>Wizard: Ask user_name, timezone, agent_name
Wizard->>Scaffold: Step 9: Create workspace files
Scaffold->>Scaffold: Write IDENTITY.md
Scaffold->>Scaffold: Write SOUL.md
Scaffold->>Scaffold: Write USER.md
Scaffold->>Scaffold: Write AGENTS.md
Scaffold->>Scaffold: Write MEMORY.md
Scaffold->>Scaffold: Create skills/ directory
Scaffold->>Scaffold: Create memory/ directory
Scaffold->>Scaffold: Create state/ directory
Scaffold-->>Wizard: Success
Wizard->>Config: Save config.toml
Config-->>Wizard: Success
Wizard->>Marker: persist_workspace_selection()
Marker->>Marker: Write active_workspace.toml
Marker-->>Wizard: Success
Wizard-->>User: Workspace ready
Quick Setup (Non-Interactive):
# Zero-prompt setup
zeroclaw onboard --api-key sk-... --provider openrouterThe quick setup flow skips interactive prompts and uses sensible defaults:
- Provider: OpenRouter (unless overridden)
-
Model:
anthropic/claude-sonnet-4.6 - Memory: SQLite with auto-save
- Security: Supervised, workspace-scoped
- Secrets: Encrypted by default
Sources: src/onboard/wizard.rs:61-196, src/onboard/wizard.rs:300-463
The workspace contains several state files that persist runtime information across agent sessions.
| File | Purpose | Format | Managed By |
|---|---|---|---|
state/models_cache.json |
Provider model catalog cache | JSON |
models refresh command |
state/cron_jobs.json |
Scheduled task definitions | JSON | CronManager |
state/heartbeat.json |
Heartbeat state and history | JSON | HeartbeatEngine |
memory/memories.db |
SQLite memory backend | SQLite3 | SqliteMemory |
Model Cache Structure:
{
"entries": [
{
"provider": "openrouter",
"models": [
"anthropic/claude-sonnet-4.6",
"openai/gpt-5.2",
"..."
]
}
]
}The model cache is written by the models refresh command and read during onboarding to provide a faster model selection experience.
Cache TTL: 12 hours (defined as MODEL_CACHE_TTL_SECS in src/onboard/wizard.rs:56)
Sources: src/channels/mod.rs:70-71, src/onboard/wizard.rs:56
ZeroClaw's workspace management system provides:
- Flexible Resolution: Environment variables, markers, or defaults
-
Multi-Profile Support: Switch between identities via
active_workspace.toml - Identity Management: OpenClaw markdown or AIEOS JSON formats
- Security by Default: Workspace scoping, forbidden paths, symlink protection
- State Persistence: Models cache, cron jobs, memory databases
The workspace is the foundation of ZeroClaw's configuration, identity, and state management. Understanding workspace resolution and scoping is essential for secure multi-tenant deployments and custom agent identities.