Files
devclaw-gitea/docs/research-context-injection.md
Lauren ten Hoor 2893ba0507 research: document bootstrap hooks for context injection (#181)
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
2026-02-14 14:03:14 +08:00

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.

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:bootstrap hook 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:

  1. Read role instructions from disk (projects/roles/{project}/{role}.md)
  2. Append to task message string
  3. 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

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:

  1. Plugin registers agent:bootstrap hook during initialization
  2. When a worker session starts, OpenClaw calls applyBootstrapHookOverrides()
  3. Hook receives bootstrapFiles array (workspace context files)
  4. Hook can add/modify/remove files dynamically
  5. Modified files are included in system prompt automatically
  6. 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
  • spawnedBy field 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?: string parameter
  • 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 from buildTaskMessage()
  • File read logic

Keep:

  • Task message construction (issue details, completion instructions)
  • Completion instructions (work_finish call template)

New Flow:

  1. dispatchTask() builds minimal task message (issue only)
  2. Session spawn/send happens (unchanged)
  3. Bootstrap hook fires during agent init (automatic)
  4. 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:

  1. Dev worker receives instructions in system prompt
  2. QA worker receives different instructions
  3. Project-specific instructions override defaults
  4. Missing instruction files fall back gracefully
  5. Non-DevClaw sessions unaffected
  6. 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

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)

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

  1. Implement Bootstrap Hook injection as the primary solution
  2. Keep task message minimal (issue details + completion template)
  3. Migrate role instruction loading to hook callback
  4. Add sessionKey parsing logic to identify DevClaw workers
  5. Test thoroughly (especially fallback paths)
  6. 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

  1. src/hooks/internal-hooks.ts — Hook event system
  2. src/agents/bootstrap-hooks.ts — Bootstrap hook application
  3. src/agents/bootstrap-files.ts — Bootstrap file resolution
  4. src/agents/workspace.ts — WorkspaceBootstrapFile type
  5. src/agents/system-prompt.ts — System prompt construction
  6. src/config/sessions/types.ts — SessionEntry definition
  7. src/gateway/sessions-patch.ts — Session patch API

DevClaw Files Modified (Proposed)

  1. index.ts — Hook registration
  2. lib/dispatch.ts — Remove file-read logic
  3. lib/bootstrap-hook.ts — New helper module (optional)
  4. docs/research-context-injection.md — This document

Next Steps

  1. Create PoC (30 min) — Validate hook fires and sessionKey parsing works
  2. Full Implementation (4-6 hrs) — Refactor dispatch.ts, add hook logic
  3. Integration Testing (2-3 hrs) — Verify dev/qa workflows unchanged
  4. Security Audit Verification (30 min) — Confirm audit no longer flags dispatch
  5. Documentation Update (1 hr) — Update AGENTS.md and README
  6. 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