-
Notifications
You must be signed in to change notification settings - Fork 4.4k
07.4 System Prompt Construction
Relevant source files
The following files were used as context for generating this wiki page:
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).
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<Box<dyn Tool>>)"]
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
Sources: src/channels/mod.rs:888-1058
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
Sources: src/channels/mod.rs:888-1058, src/channels/mod.rs:846-870
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
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
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
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
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
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
This is the most substantial section, where identity and personalization are injected. ZeroClaw supports two identity formats:
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
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 JSONWhen 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
A timezone-aware timestamp helps the model understand temporal context for scheduling, date math, and time-sensitive operations:
## Current Date & Time
Timezone: PSTThe timezone is determined via chrono::Local::now().format("%Z").
Sources: src/channels/mod.rs:1029-1032
System information for debugging and context:
## Runtime
Host: laptop.local | OS: linux | Model: anthropic/claude-sonnet-4-20250514The host is resolved via hostname::get(), OS via std::env::consts::OS, and model from the configuration.
Sources: src/channels/mod.rs:1034-1041
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
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.
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
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"]
Sources: src/channels/mod.rs:1482-1620, src/channels/mod.rs:888-1058
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
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
For smaller models (13B parameters or less), ZeroClaw supports a compact context mode that reduces bootstrap file limits:
[agent]
compact_context = trueWhen 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
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
The system prompt construction process never includes raw credentials or API keys. All sensitive values are:
-
Stored encrypted in
config.tomlwhensecrets.encrypt = true(see Secret Management) - Resolved at runtime from environment variables or encrypted storage
- Scrubbed from tool outputs before returning results to the LLM
- 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
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
To inspect the assembled system prompt:
-
Set
RUST_LOG=debugto enable debug logging - Check the first message in conversation history (always the system message)
-
Use
zeroclaw agent -m "test"with--debugflag (if available) -
Inspect workspace files at
~/.zeroclaw/workspace/*.md -
Verify identity format in
~/.zeroclaw/config.tomlunder[identity]
Common issues:
- Missing identity files: Check for "File not found" markers in system prompt
-
Truncated context: Increase
bootstrap_max_charsor reduce file sizes - Model refuses hardware requests: Verify hardware tools are registered and hardware access section is present
-
Wrong identity format: Confirm
identity.formatmatches your workspace structure
Sources: src/channels/mod.rs:888-1058, src/channels/mod.rs:1060-1103