Skip to content

04.4 Workspace Management

Nikolay Vyahhi edited this page Feb 19, 2026 · 3 revisions

Workspace Management

Relevant source files

The following files were used as context for generating this wiki page:

Purpose and Scope

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.


Workspace Directory Structure

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


Workspace Resolution Mechanism

ZeroClaw resolves the workspace directory using a three-tier resolution strategy with explicit environment variable override support.

Workspace Resolution Flow

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"]
Loading

Resolution Priority:

  1. ZEROCLAW_WORKSPACE environment variable (highest)
  2. active_workspace.toml marker file
  3. ~/.zeroclaw/workspace default 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


Multi-Profile Workspace Management

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.

Active Workspace Marker Structure

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(())
}

Multi-Profile Workflow

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
Loading

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


Bootstrap Files and Identity Management

ZeroClaw constructs its system prompt by loading markdown files from the workspace directory. These files define the agent's identity, behavior, and capabilities.

Bootstrap File Loading

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);
}

System Prompt Construction Flow

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"]
Loading

Bootstrap File Descriptions

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


Configuration File Management

The Config struct stores all system settings and is serialized to config.toml using the TOML format.

Config Loading and Initialization

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
Loading

Environment Variable Overrides

Configuration values can be overridden at runtime using environment variables. This follows the three-tier priority model:

Priority Order:

  1. Environment variables (highest)
  2. config.toml values
  3. 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 and Security

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.

Workspace Scoping Configuration

[autonomy]
level = "supervised"          # "readonly", "supervised", "full"
workspace_only = true          # Enforce workspace scoping
forbidden_paths = [
    "/etc", "/root", "/proc", "/sys", 
    "~/.ssh", "~/.gnupg", "~/.aws"
]

File Operation Security Checks

The security model enforces workspace scoping at multiple layers:

  1. Path Validation: All file operations validate paths before execution
  2. Symlink Detection: Symlinks are canonicalized and checked
  3. Forbidden Directories: 14 system directories are hardcoded as blocked
  4. 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
Loading

Forbidden System Directories

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


Workspace Initialization Workflow

The onboarding wizard creates and scaffolds the workspace with identity files and directory structure.

Onboard Wizard Workspace Setup

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
Loading

Quick Setup (Non-Interactive):

# Zero-prompt setup
zeroclaw onboard --api-key sk-... --provider openrouter

The 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


Workspace State Management

The workspace contains several state files that persist runtime information across agent sessions.

State Files

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


Summary

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.


Clone this wiki locally