Comprehensive investigation of OpenClaw-native alternatives to the file-read-network pattern in dispatch.ts that triggers security audits. Key Findings: - Bootstrap hooks are the recommended solution - Purpose-built for dynamic workspace file injection - Plugin-only implementation (no core changes needed) - Eliminates audit false positive Deliverables: - Full research document with pros/cons analysis - PoC code demonstrating implementation approach - Migration checklist and testing plan - Decision matrix comparing alternatives Recommendation: Implement agent:bootstrap hook to inject role instructions at system prompt construction time instead of appending to task message payload. Addresses issue #181
16 KiB
Research: OpenClaw-Native Context Injection Patterns
Issue: #181
Date: 2026-02-14
Author: DEV Worker (medior)
Executive Summary
Investigated OpenClaw-native alternatives to the current file-read-network pattern in dispatch.ts that triggers security audit warnings. Found a viable solution: Bootstrap Hooks — an existing OpenClaw mechanism designed for exactly this use case.
Recommended Approach
Use OpenClaw's Bootstrap Hook System to inject role instructions dynamically during agent initialization rather than appending them to the message payload.
Implementation Strategy:
- Register an
agent:bootstraphook in devclaw's plugin - Hook receives session context (sessionKey, agentId, workspaceDir)
- Dynamically adds role instructions as virtual workspace files
- Zero file I/O in dispatch path, no network-send pattern trigger
Current Approach & Problem
What We Do Now
// dispatch.ts:240-260
async function loadRoleInstructions(
workspaceDir: string, projectName: string, role: "dev" | "qa"
): Promise<string> {
const projectFile = path.join(workspaceDir, "projects", "roles", projectName, `${role}.md`);
try { return await fs.readFile(projectFile, "utf-8"); } catch { /* fallback */ }
const defaultFile = path.join(workspaceDir, "projects", "roles", "default", `${role}.md`);
try { return await fs.readFile(defaultFile, "utf-8"); } catch { /* fallback */ }
return "";
}
Flow:
- Read role instructions from disk (
projects/roles/{project}/{role}.md) - Append to task message string
- Send via CLI/Gateway RPC to worker session
Why It Triggers Audit:
- File read → network send pattern matches potential data exfiltration
- While this is intentional/legitimate, it creates audit noise
- False positives distract from real security issues
Investigation Results
1. Bootstrap Hooks (✅ RECOMMENDED)
Location: src/hooks/internal-hooks.ts, src/agents/bootstrap-hooks.ts
What It Is: An event-driven hook system that fires during agent initialization, allowing plugins to inject or modify workspace files before the system prompt is built.
Key APIs:
// Hook registration (in plugin's register() function)
import { registerInternalHook } from "openclaw/hooks/internal-hooks";
registerInternalHook("agent:bootstrap", async (event) => {
const { workspaceDir, bootstrapFiles, sessionKey } = event.context;
// Modify bootstrapFiles array to inject role instructions
if (isDevClawWorkerSession(sessionKey)) {
const roleInstructions = await loadRoleInstructions(/* ... */);
bootstrapFiles.push({
name: "WORKER_INSTRUCTIONS.md",
path: "<virtual>",
content: roleInstructions,
missing: false,
});
}
});
How It Works:
- Plugin registers
agent:bootstraphook during initialization - When a worker session starts, OpenClaw calls
applyBootstrapHookOverrides() - Hook receives
bootstrapFilesarray (workspace context files) - Hook can add/modify/remove files dynamically
- Modified files are included in system prompt automatically
- No file-read-network pattern — happens at system prompt build time
Example from OpenClaw Source:
// src/agents/bootstrap-files.ts:38-48
export async function resolveBootstrapFilesForRun(params: {
workspaceDir: string;
config?: OpenClawConfig;
sessionKey?: string;
sessionId?: string;
agentId?: string;
}): Promise<WorkspaceBootstrapFile[]> {
const bootstrapFiles = filterBootstrapFilesForSession(
await loadWorkspaceBootstrapFiles(params.workspaceDir),
sessionKey,
);
return applyBootstrapHookOverrides({
files: bootstrapFiles,
workspaceDir: params.workspaceDir,
config: params.config,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: params.agentId,
});
}
Pros:
- ✅ Purpose-built for this exact use case
- ✅ Fires at agent init (system prompt construction time)
- ✅ No file-read-network pattern in dispatch
- ✅ Session-aware (can inspect sessionKey to determine role/project)
- ✅ Clean separation: dispatch logic vs. context injection
- ✅ Virtual files supported (no disk I/O required)
- ✅ Works with existing OpenClaw architecture
Cons:
- ⚠️ Requires plugin hook registration (minor refactor)
- ⚠️ Session metadata must carry role/project info (solvable via label or sessionKey naming)
Session Identification Strategy:
DevClaw already uses deterministic session keys:
// From getSessionForLevel() in projects.ts
sessionKey = `subagent:${agentId}/${projectName}/${role}/${level}`
Parse this in the hook to extract role/project context:
const match = sessionKey.match(/^subagent:[^/]+\/([^/]+)\/([^/]+)/);
if (match) {
const [_, projectName, role] = match;
// Load appropriate instructions
}
2. Session Metadata Fields
Location: src/config/sessions/types.ts
Investigated: SessionEntry type definition
Findings:
- SessionEntry has ~40 fields (model, provider, thinkingLevel, etc.)
- No generic "metadata" or "contextData" field
spawnedByfield tracks parent session (used for sandbox scoping)- Could theoretically extend SessionEntry, but:
- Requires OpenClaw core changes (not plugin-only)
- session.patch API would need schema updates
- Not idiomatic (bootstrap files are the intended pattern)
Conclusion: Not viable for plugin-only solution.
3. Session Hooks / Memory System
Location: src/auto-reply/reply/memory-flush.ts
Findings:
- Memory system is for long-term context persistence across sessions
- Not designed for per-task dynamic context
- Would require workers to manually fetch instructions on startup
- Adds complexity vs. bootstrap hooks
Conclusion: Wrong abstraction layer for this use case.
4. System Prompt Injection
Location: src/agents/system-prompt.ts
Investigated: buildAgentSystemPrompt() parameters
Findings:
- Accepts
extraSystemPrompt?: stringparameter - However: This is set at agent run time, not per-task
- Would still require dispatch code to pass instructions → same pattern
- Bootstrap hooks are the mechanism that feeds into this
Conclusion: Bootstrap hooks are the recommended upstream injection point.
5. Alternative Patterns Considered
A. Worker-Pull Pattern
Idea: Workers fetch their own instructions on startup via a tool call.
Issues:
- Requires worker to know what to fetch (chicken-egg problem)
- Adds latency (extra tool call before work starts)
- More fragile (what if fetch fails?)
B. Central Configuration Database
Idea: Store role instructions in plugin config, load at dispatch.
Issues:
- Doesn't solve file-read-network pattern (just moves file source)
- Less flexible (config reload required for instruction updates)
- Loses per-project customization (unless config becomes massive)
C. Cron Job contextMessages Feature
Location: src/agents/tools/cron-tool.ts
What It Does: Adds recent message context to scheduled jobs.
Why It Doesn't Apply:
- For scheduled tasks, not real-time dispatch
- Still requires message content to be populated
Detailed Implementation Plan
Phase 1: Register Bootstrap Hook
File: index.ts
import { registerInternalHook, isAgentBootstrapEvent } from "openclaw/hooks/internal-hooks";
import type { WorkspaceBootstrapFile } from "openclaw/agents/workspace";
export default {
// ... existing plugin def ...
register(api: OpenClawPluginApi) {
// Existing tool/CLI/service registration...
// Register bootstrap hook for role instruction injection
registerInternalHook("agent:bootstrap", async (event) => {
if (!isAgentBootstrapEvent(event)) return;
const { sessionKey, workspaceDir, bootstrapFiles } = event.context;
// Parse sessionKey: subagent:agentId/projectName/role/level
const match = sessionKey?.match(/^subagent:[^/]+\/([^/]+)\/(dev|qa)/);
if (!match) return; // Not a DevClaw worker session
const [_, projectName, role] = match;
// Load role instructions (same logic as current loadRoleInstructions)
const instructions = await loadRoleInstructionsForHook(
workspaceDir,
projectName,
role as "dev" | "qa"
);
if (instructions) {
// Inject as virtual workspace file
bootstrapFiles.push({
name: "WORKER_INSTRUCTIONS.md" as const,
path: `<devclaw:${projectName}:${role}>`,
content: instructions,
missing: false,
});
}
});
api.logger.info("DevClaw: registered agent:bootstrap hook for role instruction injection");
}
};
Phase 2: Refactor Dispatch
File: lib/dispatch.ts
Remove:
loadRoleInstructions()function call frombuildTaskMessage()- File read logic
Keep:
- Task message construction (issue details, completion instructions)
- Completion instructions (work_finish call template)
New Flow:
dispatchTask()builds minimal task message (issue only)- Session spawn/send happens (unchanged)
- Bootstrap hook fires during agent init (automatic)
- Worker receives task message + role instructions via system prompt
Critical: Ensure completion instructions remain in task message (not bootstrap files) so they're specific to each task.
Phase 3: Helper Function
File: lib/bootstrap-hook.ts (new)
import fs from "node:fs/promises";
import path from "node:path";
/**
* Load role instructions for bootstrap hook injection.
* Same logic as original loadRoleInstructions, but in a hook-specific module.
*/
export async function loadRoleInstructionsForHook(
workspaceDir: string,
projectName: string,
role: "dev" | "qa"
): Promise<string> {
const projectFile = path.join(
workspaceDir,
"projects",
"roles",
projectName,
`${role}.md`
);
try {
return await fs.readFile(projectFile, "utf-8");
} catch {
// Fallback to default
const defaultFile = path.join(
workspaceDir,
"projects",
"roles",
"default",
`${role}.md`
);
try {
return await fs.readFile(defaultFile, "utf-8");
} catch {
return ""; // No instructions found
}
}
}
Phase 4: Testing
Scenarios:
- ✅ Dev worker receives instructions in system prompt
- ✅ QA worker receives different instructions
- ✅ Project-specific instructions override defaults
- ✅ Missing instruction files fall back gracefully
- ✅ Non-DevClaw sessions unaffected
- ✅ Security audit no longer flags dispatch.ts
Test Plan:
# 1. Pick up a dev task
devclaw work start --issue 999 --role dev --level medior
# 2. Verify worker session has instructions
openclaw session inspect subagent:devclaw/test-project/dev/medior
# 3. Check system prompt includes WORKER_INSTRUCTIONS.md
openclaw session context subagent:devclaw/test-project/dev/medior
# 4. Run security audit
openclaw audit --plugin devclaw
Pros/Cons Summary
Bootstrap Hooks (Recommended)
Pros:
- ✅ Zero changes to OpenClaw core
- ✅ Plugin-only solution
- ✅ Idiomatic (uses existing infrastructure)
- ✅ Eliminates file-read-network pattern from dispatch
- ✅ Session-aware dynamic injection
- ✅ Virtual files (no temp file creation)
- ✅ Automatic inclusion in system prompt
- ✅ Clean separation of concerns
Cons:
- ⚠️ Moderate refactor (move logic from dispatch to hook)
- ⚠️ Requires sessionKey parsing (already deterministic)
- ⚠️ Hook registration happens once (not per-task) — need robust sessionKey matching
Effort: ~4-6 hours (hook registration, refactor dispatch, testing)
Alternative: Worker-Pull (Not Recommended)
Pros:
- ✅ Explicit (worker knows it's fetching instructions)
Cons:
- ❌ Extra latency (tool call overhead)
- ❌ Fragile (fetch failures block work)
- ❌ Chicken-egg problem (how does worker know what to fetch?)
- ❌ Still requires file read somewhere
Effort: ~6-8 hours (new tool, worker logic, error handling)
Decision Matrix
| Criterion | Bootstrap Hooks | Worker-Pull | Session Metadata | Current (File-Read) |
|---|---|---|---|---|
| Plugin-only | ✅ Yes | ✅ Yes | ❌ Needs core | ✅ Yes |
| No audit trigger | ✅ Yes | ⚠️ Maybe | ✅ Yes | ❌ No |
| Idiomatic | ✅ Yes | ❌ No | ⚠️ Maybe | ⚠️ Current |
| Performance | ✅ Fast | ⚠️ +1 tool call | ✅ Fast | ✅ Fast |
| Maintainability | ✅ High | ⚠️ Medium | ❌ Core dependency | ✅ High |
| Risk | 🟢 Low | 🟡 Medium | 🔴 High | 🟡 Medium |
Winner: Bootstrap Hooks
Recommendation
- Implement Bootstrap Hook injection as the primary solution
- Keep task message minimal (issue details + completion template)
- Migrate role instruction loading to hook callback
- Add sessionKey parsing logic to identify DevClaw workers
- Test thoroughly (especially fallback paths)
- Document in AGENTS.md that instructions are injected at init, not dispatch
Timeline:
- Implementation: 4-6 hours
- Testing: 2-3 hours
- Documentation: 1 hour
- Total: ~1 working day
Security Impact:
- Eliminates false positive audit trigger
- No change to security posture (instructions are still file-sourced, just at a different layer)
- Improves audit signal-to-noise ratio
Proof of Concept
Minimal PoC Code
// PoC: Bootstrap hook registration in index.ts
registerInternalHook("agent:bootstrap", async (event) => {
if (!isAgentBootstrapEvent(event)) return;
const { sessionKey, workspaceDir, bootstrapFiles } = event.context;
const match = sessionKey?.match(/^subagent:[^/]+\/([^/]+)\/(dev|qa)/);
if (match) {
const [_, projectName, role] = match;
const instructions = `# ${role.toUpperCase()} Instructions\n\nThis is a PoC injection.`;
bootstrapFiles.push({
name: "WORKER_INSTRUCTIONS.md",
path: `<devclaw-poc>`,
content: instructions,
missing: false,
});
console.log(`[DevClaw PoC] Injected instructions for ${projectName}/${role}`);
}
});
Test:
# Start a dev worker session
openclaw session create subagent:devclaw/test/dev/medior --model claude-sonnet-4
# Check if WORKER_INSTRUCTIONS.md appears in context
openclaw session context subagent:devclaw/test/dev/medior
Expected Output: System prompt should include section:
## WORKER_INSTRUCTIONS.md
# DEV Instructions
This is a PoC injection.
References
OpenClaw Source Files Reviewed
src/hooks/internal-hooks.ts— Hook event systemsrc/agents/bootstrap-hooks.ts— Bootstrap hook applicationsrc/agents/bootstrap-files.ts— Bootstrap file resolutionsrc/agents/workspace.ts— WorkspaceBootstrapFile typesrc/agents/system-prompt.ts— System prompt constructionsrc/config/sessions/types.ts— SessionEntry definitionsrc/gateway/sessions-patch.ts— Session patch API
DevClaw Files Modified (Proposed)
index.ts— Hook registrationlib/dispatch.ts— Remove file-read logiclib/bootstrap-hook.ts— New helper module (optional)docs/research-context-injection.md— This document
Next Steps
- Create PoC (30 min) — Validate hook fires and sessionKey parsing works
- Full Implementation (4-6 hrs) — Refactor dispatch.ts, add hook logic
- Integration Testing (2-3 hrs) — Verify dev/qa workflows unchanged
- Security Audit Verification (30 min) — Confirm audit no longer flags dispatch
- Documentation Update (1 hr) — Update AGENTS.md and README
- PR & Review (1-2 hrs) — Submit for review
Total Effort: ~1-1.5 working days
Appendix: OpenClaw Hook Event Structure
export interface InternalHookEvent {
type: "command" | "session" | "agent" | "gateway";
action: string; // "bootstrap" for agent:bootstrap
sessionKey: string;
context: Record<string, unknown>;
timestamp: Date;
messages: string[]; // Can push confirmation messages
}
export type AgentBootstrapHookContext = {
workspaceDir: string;
bootstrapFiles: WorkspaceBootstrapFile[];
cfg?: OpenClawConfig;
sessionKey?: string;
sessionId?: string;
agentId?: string;
};
export type WorkspaceBootstrapFile = {
name: string; // E.g., "AGENTS.md", "WORKER_INSTRUCTIONS.md"
path: string; // File path or "<virtual>" marker
content?: string; // File content (if loaded)
missing: boolean; // True if file doesn't exist
};
End of Research Document