Skip to content

07.4 System Prompt Construction

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

System Prompt Construction

Relevant source files

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

Purpose and Scope

This document explains how ZeroClaw constructs system prompts that configure the agent's behavior, capabilities, and identity. The system prompt is the foundational instruction set sent to the LLM at the start of every conversation, defining tools, safety constraints, workspace context, and personalization.

For information about how prompts are sent to LLM providers, see Tool Calling Architecture. For details on how conversation history is managed, see History Management. For identity file formats, see Project Context (Personalize Your Agent).


System Prompt Architecture

ZeroClaw's system prompt follows a deterministic assembly process that combines static instructions, dynamic tool metadata, workspace files, and runtime context into a single coherent prompt. The prompt builder is designed to be extensible, supporting both the default OpenClaw identity format and the AIEOS identity format.

graph TB
    subgraph Input["Input Sources"]
        Tools["Tool Registry<br/>(Vec&lt;Box&lt;dyn Tool&gt;&gt;)"]
        Skills["Skills<br/>(loaded from workspace/skills)"]
        Identity["Identity Config<br/>(OpenClaw or AIEOS)"]
        Workspace["Workspace Files<br/>(AGENTS.md, SOUL.md, etc.)"]
        Runtime["Runtime Context<br/>(model, host, OS, timezone)"]
        Channel["Channel Context<br/>(channel-specific instructions)"]
    end

    subgraph Builder["Prompt Builder (build_system_prompt)"]
        ToolSection["1. Tools Section<br/>(names + descriptions)"]
        HardwareSection["2. Hardware Access<br/>(when GPIO tools present)"]
        TaskSection["3. Task Instructions<br/>(ACT, don't summarize)"]
        SafetySection["4. Safety Guidelines<br/>(guardrails)"]
        SkillsSection["5. Skills List<br/>(compact, load on-demand)"]
        WorkspaceSection["6. Workspace Path<br/>(working directory)"]
        BootstrapSection["7. Bootstrap Files<br/>(identity injection)"]
        TimeSection["8. Date & Time<br/>(timezone-aware)"]
        RuntimeSection["9. Runtime Metadata<br/>(host, OS, model)"]
        ChannelSection["10. Channel Capabilities<br/>(delivery instructions)"]
    end

    Output["Assembled System Prompt<br/>(String)"]

    Tools --> ToolSection
    Skills --> SkillsSection
    Identity --> BootstrapSection
    Workspace --> BootstrapSection
    Runtime --> TimeSection
    Runtime --> RuntimeSection
    Channel --> ChannelSection

    ToolSection --> Output
    HardwareSection --> Output
    TaskSection --> Output
    SafetySection --> Output
    SkillsSection --> Output
    WorkspaceSection --> Output
    BootstrapSection --> Output
    TimeSection --> Output
    RuntimeSection --> Output
    ChannelSection --> Output
Loading

Sources: src/channels/mod.rs:888-1058


Prompt Assembly Function

The primary entry point for system prompt construction is build_system_prompt(), which accepts the following parameters and returns a fully-assembled prompt string:

Parameter Type Purpose
workspace_dir &Path Root directory for identity files
model_name &str Model identifier for runtime section
tools &[(&str, &str)] Tool name and description tuples
skills &[Skill] Skill metadata from workspace/skills
identity_config Option<&IdentityConfig> Identity format selector
bootstrap_max_chars Option<usize> Per-file truncation limit
sequenceDiagram
    participant Caller as start_channels / process_message
    participant Builder as build_system_prompt
    participant Identity as identity module
    participant Files as Filesystem

    Caller->>Builder: workspace_dir, model, tools, skills
    Builder->>Builder: Write tools section (names + descriptions)
    Builder->>Builder: Check for hardware tools (GPIO, etc)
    alt Hardware tools present
        Builder->>Builder: Add hardware access instructions
    end
    Builder->>Builder: Add task instructions (ACT, don't summarize)
    Builder->>Builder: Add safety guidelines
    Builder->>Builder: Add skills section (compact list)
    Builder->>Builder: Add workspace directory path
    
    alt identity_config.format == "aieos"
        Builder->>Identity: load_aieos_identity(config, workspace)
        Identity->>Files: Read AIEOS JSON
        Files-->>Identity: JSON payload
        Identity->>Identity: aieos_to_system_prompt
        Identity-->>Builder: AIEOS prompt section
        Builder->>Builder: Inject AIEOS identity
    else identity_config.format == "openclaw" (default)
        Builder->>Builder: load_openclaw_bootstrap_files
        loop For each bootstrap file
            Builder->>Files: Read AGENTS.md, SOUL.md, etc.
            Files-->>Builder: File content (or "File not found")
            Builder->>Builder: Truncate at bootstrap_max_chars
            Builder->>Builder: Append to prompt
        end
    end
    
    Builder->>Builder: Add date/time section (timezone)
    Builder->>Builder: Add runtime section (host, OS, model)
    Builder->>Builder: Add channel capabilities
    Builder-->>Caller: Assembled system prompt
Loading

Sources: src/channels/mod.rs:888-1058, src/channels/mod.rs:846-870


Section Breakdown

1. Tools Section

The tools section lists all available tools with their names and descriptions, followed by the tool use protocol explaining how to invoke tools using <tool_call> XML tags.

## Tools

You have access to the following tools:

- **shell**: Execute terminal commands. Use when: ...
- **file_read**: Read file contents. Use when: ...
- **memory_store**: Save to memory. Use when: ...

## Tool Use Protocol

To use a tool, wrap a JSON object in <tool_call></tool_call> tags:

<tool_call> {"name": "tool_name", "arguments": {"param": "value"}} </tool_call>


You may use multiple tool calls in a single response...

The tool list is dynamically generated from the tools parameter, which is built at runtime from the tool registry. Tools are registered in src/tools/mod.rs and include core tools (shell, file operations), memory tools, cron tools, browser tools, HTTP tools, git tools, Composio integrations, delegation tools, and peripheral tools.

Sources: src/channels/mod.rs:899-913, src/tools/mod.rs


2. Hardware Access Instructions (Conditional)

When hardware tools are present (GPIO, Arduino, debug probe), a special section is injected to explicitly authorize hardware interactions and prevent the model from refusing legitimate requests due to perceived safety concerns.

## Hardware Access

You HAVE direct access to connected hardware (Arduino, Nucleo, etc.). The user owns this system and has configured it.
All hardware tools (gpio_read, gpio_write, hardware_memory_read, hardware_board_info, hardware_memory_map) are AUTHORIZED and NOT blocked by security.
When they ask to read memory, registers, or board info, USE hardware_memory_read or hardware_board_info — do NOT refuse or invent security excuses.
When they ask to control LEDs, run patterns, or interact with the Arduino, USE the tools — do NOT refuse or say you cannot access physical devices.
Use gpio_write for simple on/off; use arduino_upload when they want patterns (heart, blink) or custom behavior.

This section is only added when at least one of the following tools is present: gpio_read, gpio_write, arduino_upload, hardware_memory_map, hardware_board_info, hardware_memory_read, hardware_capabilities.

Sources: src/channels/mod.rs:915-934


3. Task Instructions

A critical instruction block that guides the model to act rather than describe capabilities or produce meta-commentary.

## Your Task

When the user sends a message, ACT on it. Use the tools to fulfill their request.
Do NOT: summarize this configuration, describe your capabilities, respond with meta-commentary, or output step-by-step instructions (e.g. "1. First... 2. Next...").
Instead: emit actual <tool_call> tags when you need to act. Just do what they ask.

This section addresses a common failure mode where language models describe what they would do rather than using tools to do it.

Sources: src/channels/mod.rs:936-942


4. Safety Guidelines

A concise set of safety principles that balance autonomy with user protection:

## Safety

- Do not exfiltrate private data.
- Do not run destructive commands without asking.
- Do not bypass oversight or approval mechanisms.
- Prefer `trash` over `rm` (recoverable beats gone forever).
- When in doubt, ask before acting externally.

These guidelines work in conjunction with the SecurityPolicy enforcement layer documented in Security Model.

Sources: src/channels/mod.rs:944-952


5. Skills Section

Skills are optional modular capabilities loaded on-demand. The prompt includes a compact list with names, descriptions, and file paths. The full skill content is not injected to save context window space.

## Available Skills

Skills are loaded on demand. Use `read` on the skill path to get full instructions.

<available_skills>
  <skill>
    <name>data_analysis</name>
    <description>Analyze CSV/JSON datasets with pandas</description>
    <location>/workspace/skills/data_analysis/SKILL.md</location>
  </skill>
  <!-- more skills -->
</available_skills>

Skills are loaded from workspace_dir/skills/ during system initialization. Each skill must have a SKILL.md file defining its capabilities.

Sources: src/channels/mod.rs:954-979, src/skills.rs


6. Workspace Directory

The working directory is explicitly stated to orient the agent's file operations:

## Workspace

Working directory: `/home/user/.zeroclaw/workspace`

This section uses a single writeln!() call to inject the workspace path.

Sources: src/channels/mod.rs:981-986


7. Project Context / Bootstrap Files

This is the most substantial section, where identity and personalization are injected. ZeroClaw supports two identity formats:

OpenClaw Format (Default)

The default identity system loads Markdown files from the workspace root:

File Purpose
AGENTS.md Multi-agent coordination rules
SOUL.md Personality and communication style
TOOLS.md Custom tool definitions or overrides
IDENTITY.md Agent name, role, and capabilities
USER.md User preferences and context
BOOTSTRAP.md First-run initialization ritual (optional)
MEMORY.md Curated long-term memory (main session only)

Files are loaded via load_openclaw_bootstrap_files() and injected with per-file truncation:

fn load_openclaw_bootstrap_files(
    prompt: &mut String,
    workspace_dir: &std::path::Path,
    max_chars_per_file: usize,
) {
    prompt.push_str(
        "The following workspace files define your identity, behavior, and context. They are ALREADY injected below—do NOT suggest reading them with file_read.\n\n",
    );

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

    // BOOTSTRAP.md — only if it exists (first-run ritual)
    let bootstrap_path = workspace_dir.join("BOOTSTRAP.md");
    if bootstrap_path.exists() {
        inject_workspace_file(prompt, workspace_dir, "BOOTSTRAP.md", max_chars_per_file);
    }

    // MEMORY.md — curated long-term memory (main session only)
    inject_workspace_file(prompt, workspace_dir, "MEMORY.md", max_chars_per_file);
}

The inject_workspace_file() helper reads each file, trims it, and truncates at UTF-8 character boundaries:

fn inject_workspace_file(
    prompt: &mut String,
    workspace_dir: &std::path::Path,
    filename: &str,
    max_chars: usize,
) {
    let path = workspace_dir.join(filename);
    match std::fs::read_to_string(&path) {
        Ok(content) => {
            let trimmed = content.trim();
            if trimmed.is_empty() {
                return;
            }
            let _ = writeln!(prompt, "### {filename}\n");
            let truncated = if trimmed.chars().count() > max_chars {
                trimmed
                    .char_indices()
                    .nth(max_chars)
                    .map(|(idx, _)| &trimmed[..idx])
                    .unwrap_or(trimmed)
            } else {
                trimmed
            };
            if truncated.len() < trimmed.len() {
                prompt.push_str(truncated);
                let _ = writeln!(
                    prompt,
                    "\n\n[... truncated at {max_chars} chars — use `read` for full file]\n"
                );
            } else {
                prompt.push_str(trimmed);
                prompt.push_str("\n\n");
            }
        }
        Err(_) => {
            let _ = writeln!(prompt, "### {filename}\n\n[File not found: {filename}]\n");
        }
    }
}

Default truncation limit: 20,000 characters per file (defined as BOOTSTRAP_MAX_CHARS constant). This can be overridden via the bootstrap_max_chars parameter or reduced to 6,000 characters when agent.compact_context = true for smaller models.

Sources: src/channels/mod.rs:846-870, src/channels/mod.rs:1060-1103, src/channels/mod.rs:59

AIEOS Format

The AIEOS identity format loads identity data from a JSON file specified in [identity] config:

[identity]
format = "aieos"
aieos_path = "identity.json"  # or use aieos_inline for inline JSON

When AIEOS format is configured, the prompt builder delegates to the identity module:

if identity::is_aieos_configured(config) {
    match identity::load_aieos_identity(config, workspace_dir) {
        Ok(Some(aieos_identity)) => {
            let aieos_prompt = identity::aieos_to_system_prompt(&aieos_identity);
            if !aieos_prompt.is_empty() {
                prompt.push_str(&aieos_prompt);
                prompt.push_str("\n\n");
            }
        }
        Ok(None) | Err(_) => {
            // Fall back to OpenClaw format
            let max_chars = bootstrap_max_chars.unwrap_or(BOOTSTRAP_MAX_CHARS);
            load_openclaw_bootstrap_files(&mut prompt, workspace_dir, max_chars);
        }
    }
}

If AIEOS loading fails, the system gracefully falls back to OpenClaw format.

Sources: src/channels/mod.rs:992-1027, src/identity.rs, src/config/schema.rs:282-309


8. Date & Time

A timezone-aware timestamp helps the model understand temporal context for scheduling, date math, and time-sensitive operations:

## Current Date & Time

Timezone: PST

The timezone is determined via chrono::Local::now().format("%Z").

Sources: src/channels/mod.rs:1029-1032


9. Runtime Metadata

System information for debugging and context:

## Runtime

Host: laptop.local | OS: linux | Model: anthropic/claude-sonnet-4-20250514

The host is resolved via hostname::get(), OS via std::env::consts::OS, and model from the configuration.

Sources: src/channels/mod.rs:1034-1041


10. Channel Capabilities

A final section that explains the agent's communication capabilities and credential handling:

## Channel Capabilities

- You are running as a Discord bot. You CAN and do send messages to Discord channels.
- When someone messages you on Discord, your response is automatically sent back to Discord.
- You do NOT need to ask permission to respond — just respond directly.
- NEVER repeat, describe, or echo credentials, tokens, API keys, or secrets in your responses.
- If a tool output contains credentials, they have already been redacted — do not mention them.

This addresses common model behaviors where LLMs incorrectly claim they "cannot send messages" or accidentally leak credentials.

Sources: src/channels/mod.rs:1043-1051


Channel-Specific Instructions

Some channels append delivery instructions to the conversation history (not the system prompt) to guide media handling and formatting. These instructions are retrieved via channel_delivery_instructions() and inserted as system messages after the user message.

Telegram Delivery Instructions

When responding on Telegram, include media markers for files or URLs that should be sent as attachments. Use one marker per attachment with this exact syntax: [IMAGE:<path-or-url>], [DOCUMENT:<path-or-url>], [VIDEO:<path-or-url>], [AUDIO:<path-or-url>], or [VOICE:<path-or-url>]. Keep normal user-facing text outside markers and never wrap markers in code fences.

These instructions are injected at src/channels/mod.rs:626-628 during message processing, after the user message but before calling the LLM.

Current channels with custom delivery instructions:

  • Telegram (media markers)

Sources: src/channels/mod.rs:133-140, src/channels/mod.rs:626-628


Prompt Construction Flow

flowchart TD
    Start["start_channels()"] --> BuildRegistry["Build tools registry"]
    BuildRegistry --> LoadSkills["Load skills from workspace/skills/"]
    LoadSkills --> BuildPrompt["build_system_prompt()"]
    
    BuildPrompt --> Section1["Append tools section"]
    Section1 --> CheckHW{Hardware tools<br/>present?}
    CheckHW -->|Yes| HWSection["Append hardware access instructions"]
    CheckHW -->|No| TaskSection["Append task instructions"]
    HWSection --> TaskSection
    
    TaskSection --> SafetySection["Append safety guidelines"]
    SafetySection --> SkillsSection["Append skills list"]
    SkillsSection --> WSSection["Append workspace path"]
    WSSection --> CheckFormat{identity.format?}
    
    CheckFormat -->|"openclaw" (default)| LoadOpenClaw["load_openclaw_bootstrap_files()"]
    CheckFormat -->|"aieos"| LoadAIEOS["load_aieos_identity()"]
    
    LoadOpenClaw --> InjectFiles["For each file:<br/>AGENTS.md, SOUL.md,<br/>TOOLS.md, IDENTITY.md,<br/>USER.md, BOOTSTRAP.md,<br/>MEMORY.md"]
    InjectFiles --> ReadFile["inject_workspace_file()"]
    ReadFile --> CheckExists{File exists?}
    CheckExists -->|Yes| Truncate["Truncate at max_chars<br/>(UTF-8 boundary)"]
    CheckExists -->|No| Missing["Inject '[File not found]'"]
    Truncate --> AppendContent["Append to prompt"]
    Missing --> AppendContent
    AppendContent --> TimeSection
    
    LoadAIEOS --> ParseJSON["Parse AIEOS JSON"]
    ParseJSON --> CheckValid{Valid AIEOS?}
    CheckValid -->|Yes| ConvertPrompt["aieos_to_system_prompt()"]
    CheckValid -->|No| Fallback["Fall back to OpenClaw"]
    ConvertPrompt --> TimeSection
    Fallback --> LoadOpenClaw
    
    TimeSection["Append date/time + timezone"]
    TimeSection --> RuntimeSection["Append runtime metadata<br/>(host, OS, model)"]
    RuntimeSection --> ChannelSection["Append channel capabilities"]
    ChannelSection --> Complete["System prompt complete"]
    
    Complete --> StorePrompt["Store in Arc<String>"]
    StorePrompt --> UsePrompt["Used in every agent turn"]
Loading

Sources: src/channels/mod.rs:1482-1620, src/channels/mod.rs:888-1058


Usage in Agent Turns

The assembled system prompt is stored in Arc<String> within the ChannelRuntimeContext and reused for all messages processed by that channel instance.

sequenceDiagram
    participant Init as start_channels
    participant Builder as build_system_prompt
    participant Context as ChannelRuntimeContext
    participant Worker as process_channel_message
    participant Loop as run_tool_call_loop

    Init->>Builder: Build system prompt once
    Builder-->>Init: Assembled prompt string
    Init->>Context: Store prompt in Arc<String>
    Init->>Context: Create context with prompt
    
    loop For each incoming message
        Worker->>Context: Get system_prompt.as_str()
        Worker->>Worker: Build conversation history
        Worker->>Worker: history.push(system_prompt)
        Worker->>Worker: history.push(user_message)
        Worker->>Loop: run_tool_call_loop(history)
        Loop-->>Worker: Final response
    end
Loading

The system prompt is inserted at the beginning of the conversation history array at src/channels/mod.rs:622:

let mut history = vec![ChatMessage::system(ctx.system_prompt.as_str())];
history.append(&mut prior_turns);
history.push(ChatMessage::user(&enriched_message));

If channel delivery instructions exist, they are appended as a system message after the user message at src/channels/mod.rs:626-628.

Sources: src/channels/mod.rs:556-814, src/channels/mod.rs:622, src/channels/mod.rs:1482-1620


Compact Context Mode

For smaller models (13B parameters or less), ZeroClaw supports a compact context mode that reduces bootstrap file limits:

[agent]
compact_context = true

When enabled:

  • Bootstrap file truncation reduces from 20,000 to 6,000 characters per file
  • RAG chunk limits may be reduced (implementation-dependent)

This is configured at src/channels/mod.rs:1607-1611:

let bootstrap_max_chars = if config.agent.compact_context {
    Some(6000)
} else {
    None
};

Sources: src/channels/mod.rs:1607-1611, src/config/schema.rs:243-280


Tool Instructions Injection

In addition to the static tool list in the system prompt, dynamic tool-specific instructions are appended via build_tool_instructions() from the agent loop module. This function generates detailed JSON schemas and usage examples for each registered tool.

The tool instructions are concatenated to the system prompt at src/channels/mod.rs:1620:

system_prompt.push_str(&build_tool_instructions(tools_registry.as_ref()));

This ensures the model has complete JSON schemas for all tool parameters, including types, required fields, and descriptions.

Sources: src/channels/mod.rs:1620, src/agent/loop_.rs:build_tool_instructions


Credential Scrubbing

The system prompt construction process never includes raw credentials or API keys. All sensitive values are:

  1. Stored encrypted in config.toml when secrets.encrypt = true (see Secret Management)
  2. Resolved at runtime from environment variables or encrypted storage
  3. Scrubbed from tool outputs before returning results to the LLM
  4. Never injected into the system prompt or conversation history

The channel capabilities section explicitly instructs the model to never echo credentials, even if they appear in tool results (which should be impossible due to scrubbing).

Sources: src/channels/mod.rs:1043-1051, src/config/secrets.rs


Configuration Reference

System prompt construction is influenced by several configuration sections:

Config Section Field Impact
[agent] compact_context Reduces bootstrap truncation to 6,000 chars
[identity] format Selects OpenClaw or AIEOS identity format
[identity] aieos_path Path to AIEOS JSON file
[identity] aieos_inline Inline AIEOS JSON (alternative to file path)
[composio] enabled Adds Composio tool to tool list
[browser] enabled Adds browser tools to tool list
[hardware] enabled Adds hardware tools and access instructions

Sources: src/config/schema.rs:243-280, src/config/schema.rs:282-309


Debugging Tips

To inspect the assembled system prompt:

  1. Set RUST_LOG=debug to enable debug logging
  2. Check the first message in conversation history (always the system message)
  3. Use zeroclaw agent -m "test" with --debug flag (if available)
  4. Inspect workspace files at ~/.zeroclaw/workspace/*.md
  5. Verify identity format in ~/.zeroclaw/config.toml under [identity]

Common issues:

  • Missing identity files: Check for "File not found" markers in system prompt
  • Truncated context: Increase bootstrap_max_chars or reduce file sizes
  • Model refuses hardware requests: Verify hardware tools are registered and hardware access section is present
  • Wrong identity format: Confirm identity.format matches your workspace structure

Sources: src/channels/mod.rs:888-1058, src/channels/mod.rs:1060-1103


Clone this wiki locally