Skip to content

feat(agents): add contextInjection mode to skip workspace files on subsequent messages#40372

Closed
dsantoreis wants to merge 5 commits into
openclaw:mainfrom
dsantoreis:feat/9157-context-injection-v2
Closed

feat(agents): add contextInjection mode to skip workspace files on subsequent messages#40372
dsantoreis wants to merge 5 commits into
openclaw:mainfrom
dsantoreis:feat/9157-context-injection-v2

Conversation

@dsantoreis

@dsantoreis dsantoreis commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds agents.defaults.contextInjection config option that controls when workspace bootstrap files (AGENTS.md, SOUL.md, USER.md, TOOLS.md, etc.) are injected into the system prompt:

Mode Behavior Token Impact
"always" (default) Current behavior — inject every message ~35,600 tokens/message
"first-message-only" Skip injection when session already has assistant messages ~35,600 tokens once, then 0

Over a 100-message conversation with "first-message-only", this saves ~3.4M tokens and ~$1.51 per session. The agent can still read workspace files on demand when needed.

This is a v2 of #28072, rewritten against current main with all review feedback addressed:

  • @bmendonca3: replaced sessionFileExists proxy with actual transcript JSONL scanning for assistant role entries — pre-created, aborted, or empty sessions are handled correctly
  • @darfaz: added security angle — workspace files often contain API keys, credentials, and personal data that were being sent to the LLM provider on every message

Configuration

{
  "agents": {
    "defaults": {
      "contextInjection": "first-message-only"
    }
  }
}

What changed

  • src/agents/bootstrap-files.ts: added sessionHasAssistantMessages() — reads first 16KB of session JSONL (sync, fast) checking for "role":"assistant" entries. Added applyContextInjectionFilter() that skips all context files when mode is "first-message-only" and assistant history exists. Wired into resolveBootstrapFilesForRun and resolveBootstrapContextForRun.
  • src/agents/pi-embedded-runner/run/attempt.ts: resolves contextInjection from config, calls sessionHasAssistantMessages only when mode is "first-message-only" (zero overhead in default mode), passes both values to bootstrap resolution.
  • src/agents/pi-embedded-runner/run/params.ts: added contextInjection to EmbeddedRunParams.
  • src/config/zod-schema.agent-defaults.ts, types.agent-defaults.ts, schema.labels.ts, schema.help.ts: schema, type, label, and help text for the new option.

What did NOT change

  • Default behavior ("always") is unchanged — no breaking changes
  • Bootstrap metadata is still loaded for workspaceNotes detection and systemPromptReport even when file content is skipped
  • Lightweight/heartbeat context mode is unaffected — contextInjection is applied as a second-pass filter after the existing contextMode filter
  • No runtime model or failover changes

Linked Issues

Validation

pnpm vitest run src/agents/bootstrap-files.context-injection.test.ts  # 12/12 pass
pnpm build                                                             # clean

Tests

12 tests in src/agents/bootstrap-files.context-injection.test.ts:

applyContextInjectionFilter:

  • always mode includes all files (with and without history)
  • first-message-only includes files when no assistant history
  • first-message-only excludes files when assistant history exists
  • undefined mode (default) behaves like always

sessionHasAssistantMessages:

  • nonexistent file returns false
  • empty file returns false
  • user-only messages returns false
  • file with assistant message returns true
  • malformed lines handled gracefully

End-to-end resolveBootstrapContextForRun:

  • first-message-only skips context when hasExistingAssistantMessages is true
  • first-message-only includes context when hasExistingAssistantMessages is false

Impact

  • 93.5% token savings over long conversations when enabled
  • Security: reduces credential/personal data exposure from N×per-message to 1×per-session
  • Zero overhead in default mode — transcript scanning only runs when "first-message-only" is configured
  • 8 files changed, 247 insertions

AI-assisted (Claude). Fully tested and reviewed.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M labels Mar 8, 2026
@greptile-apps

greptile-apps Bot commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a contextInjection config option ("always" | "first-message-only") that allows workspace bootstrap files (AGENTS.md, SOUL.md, etc.) to be injected into the system prompt only on the first message of a session, saving significant tokens on long conversations. The feature is well-structured with zero overhead in the default "always" mode and is backed by 12 tests.

Key findings:

  • Logic issue — workspaceNotes commit reminder is silently dropped after message 1: The PR description explicitly states "Bootstrap metadata is still loaded for workspaceNotes detection… even when file content is skipped", but the implementation applies applyContextInjectionFilter inside resolveBootstrapFilesForRun before bootstrapFiles is returned. This means the workspaceNotes check in attempt.ts:838 always evaluates to false on subsequent messages when first-message-only is active — the "Reminder: commit your changes in this workspace after edits." line is never injected into the system prompt past message 1.

  • Synchronous file I/O blocks the event loop: sessionHasAssistantMessages uses fsSync.openSync / fsSync.readSync, which are blocking calls. In deployments with first-message-only enabled, this will stall the event loop on every agent message. Converting to async/await with fs.promises is straightforward given the already-async call sites.

  • Inline type duplication: params.ts redeclares the "always" | "first-message-only" union instead of importing the exported ContextInjectionMode type from bootstrap-files.ts, creating a maintenance risk if new modes are added.

Confidence Score: 2/5

  • Not safe to merge without addressing the workspaceNotes metadata loss on subsequent messages, which contradicts explicit PR claims.
  • The default behavior ("always" mode) is fully backward-compatible and untouched, but the new "first-message-only" mode has a critical metadata gap: the workspaceNotes commit reminder is silently dropped from the system prompt on all messages after the first, which directly contradicts the PR description's guarantee that "Bootstrap metadata is still loaded for workspaceNotes detection… even when file content is skipped." Additionally, blocking synchronous file I/O on every message in non-default mode will stall the event loop in concurrent deployments. The duplicate type union in params.ts increases maintenance risk. All three issues are fixable before or after merge, but the metadata gap in particular contradicts an explicit PR claim.
  • src/agents/bootstrap-files.ts (filtering order and sync I/O), src/agents/pi-embedded-runner/run/params.ts (type duplication)

Last reviewed commit: 04a876a

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04a876ad0a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/bootstrap-files.ts Outdated
Comment thread src/agents/bootstrap-files.ts
Comment thread src/agents/pi-embedded-runner/run/params.ts Outdated
Comment thread src/agents/bootstrap-files.ts Outdated
@dsantoreis

This comment was marked as spam.

@dsantoreis

This comment was marked as spam.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 00a27604f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/bootstrap-files.ts
@dsantoreis

This comment was marked as spam.

@dsantoreis

This comment was marked as spam.

@dsantoreis

This comment was marked as spam.

@dsantoreis

This comment was marked as spam.

Daniel dos Santos Reis added 2 commits March 9, 2026 12:35
…bsequent messages

Add agents.defaults.contextInjection config option with two modes:
- "always" (default): current behavior, inject every message
- "first-message-only": skip workspace file injection when the session
  transcript already contains assistant messages

Detection reads the first 16KB of the session JSONL file to check for
assistant role entries (sync, fast, no full transcript load). This
addresses review feedback from openclaw#28072 where sessionFileExists was too
coarse a proxy — pre-created, aborted, or empty sessions with existing
files but no real assistant turns are handled correctly.

Security benefit: workspace files (AGENTS.md, SOUL.md, USER.md, TOOLS.md)
often contain API keys, personal data, and infrastructure details. With
"always" mode these are sent to the LLM provider on every single message
of a conversation. "first-message-only" reduces that exposure surface
by ~93.5% over a 100-message session.

12 targeted tests cover: filter modes, transcript scanning (empty,
user-only, with-assistant, malformed), and end-to-end bootstrap context
resolution with both modes.

Closes openclaw#9157
Problem: first-message-only context injection dropped bootstrap metadata after message 1 and used sync file I/O for history detection, leading to missing workspace commit reminders and event-loop blocking reads.

Root cause: context filtering was applied before metadata-dependent checks and session history probing used fs sync APIs.

Fix: make session history detection async, keep metadata checks by resolving bootstrap files in always mode when needed, and reuse ContextInjectionMode type in run params.

Testing: pnpm test src/agents/bootstrap-files.context-injection.test.ts; pnpm check

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a6ec12d527

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/pi-embedded-runner/run/attempt.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ab5f95ed9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/pi-embedded-runner/run/attempt.ts
@dsantoreis

This comment was marked as spam.

@dsantoreis

This comment was marked as spam.

@dsantoreis

This comment was marked as spam.

@dsantoreis

This comment was marked as spam.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b2edaf4f60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/pi-embedded-runner/run/attempt.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e18b57996

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/cli-runner.ts Outdated
Comment on lines +113 to +116
const hasExistingAssistantMessages =
contextInjection === "first-message-only"
? await sessionHasAssistantMessages(params.sessionFile)
: false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist CLI transcript before gating context injection

Fresh evidence in this revision is that runCliAgent now gates on sessionHasAssistantMessages(params.sessionFile), but the CLI execution path does not write assistant turns into that JSONL transcript (the append logic in commands/agent.ts is ACP-specific via persistAcpTurnTranscript, and non-delivery CLI runs do not mirror assistant output back to the transcript). In contextInjection: "first-message-only" sessions, this leaves hasExistingAssistantMessages false on follow-up CLI turns, so workspace bootstrap files keep being injected every turn instead of being skipped.

Useful? React with 👍 / 👎.

This comment was marked as spam.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 245511d0ff

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/bootstrap-files.ts Outdated
}): WorkspaceBootstrapFile[] {
const mode = params.contextInjection ?? "always";
if (mode === "first-message-only" && params.hasExistingAssistantMessages) {
return params.files.filter((file) => file.name === DEFAULT_AGENTS_FILENAME);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep HEARTBEAT.md in first-message-only follow-up runs

In first-message-only mode, this filter keeps only AGENTS.md, but lightweight heartbeat runs intentionally reduce bootstrap files to HEARTBEAT.md first (applyContextModeFilter), so follow-up heartbeat polls end up with an empty injected context once any assistant history exists. This silently drops user heartbeat instructions for sessions that enable both heartbeat.lightContext and agents.defaults.contextInjection: "first-message-only", changing heartbeat behavior after the first assistant turn.

Useful? React with 👍 / 👎.

This comment was marked as spam.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfaffc277b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/cli-runner.ts Outdated
Comment on lines +126 to +127
const hasExistingAssistantMessages =
contextInjection === "first-message-only" ? Boolean(cliSessionIdToUse?.trim()) : false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Base first-message-only skip on verified CLI continuity

hasExistingAssistantMessages is currently inferred from Boolean(cliSessionIdToUse?.trim()), which treats any non-empty stored CLI session ID as proof of prior assistant turns. In this same function, the returned agentMeta.sessionId falls back to params.sessionId when the backend output has no parsed session ID, so backends that emit text/no session token can still persist a non-empty ID and trigger follow-up skips. With agents.defaults.contextInjection="first-message-only", those follow-up runs can drop bootstrap context even though the backend session is actually fresh (or unresumable), causing missing AGENTS/SOUL instructions on subsequent turns for affected CLI backends.

Useful? React with 👍 / 👎.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c43df5ab4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/bootstrap-files.ts Outdated
const mode = params.contextInjection ?? "always";
if (mode === "first-message-only" && params.hasExistingAssistantMessages) {
const agentsFiles = params.files.filter((file) => file.name === DEFAULT_AGENTS_FILENAME);
return agentsFiles.length > 0 ? agentsFiles : params.files;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce first-message-only when AGENTS.md is filtered out

In first-message-only mode with existing assistant history, applyContextInjectionFilter falls back to returning the full bootstrap list when no AGENTS.md entry is present. Because bootstrap hooks can replace context.bootstrapFiles entirely, any hook that omits AGENTS.md (for example, to curate a custom subset) will cause all files to be injected again on follow-up turns, silently disabling the token/privacy behavior this mode is meant to provide.

Useful? React with 👍 / 👎.

This comment was marked as spam.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5f33dd8996

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/cli-runner.ts Outdated
agentMeta: {
sessionId: output.sessionId ?? params.sessionId ?? "",
sessionId: output.sessionId ?? "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear expired CLI session id when retry yields no session token

After the session_expired retry path succeeds, agentMeta.sessionId is now set to output.sessionId ?? "", so backends that return text (or otherwise omit a parseable session id) report an empty id even though the run recovered. In updateSessionStoreAfterAgentRun (src/commands/agent/session-store.ts), empty ids are ignored, so the stale expired id remains in cliSessionIds and the next turn reuses it, repeatedly triggering the session_expired failure/retry cycle for those backends. This regression was introduced by removing the fallback id in the retry return path.

Useful? React with 👍 / 👎.

This comment was marked as spam.

@darfaz

darfaz commented Mar 12, 2026

Copy link
Copy Markdown

This is directly relevant to setups with large workspace files (SOUL.md, USER.md, TOOLS.md, AGENTS.md) — the per-message token overhead is real and adds up in longer sessions.

Notes from reviewing:

Positive:

  • The first-message-only semantics are clean — opt-in is the right approach vs. changing the default
  • The HEARTBEAT.md fix (dfaffc277) is important: heartbeat-light-context sessions would have silently dropped their instructions after the first assistant turn
  • CI is now fully green — 23 passing, 0 failing

One open question before merge:
The remaining bot P2 about CLI backends that emit a synthetic fallback session ID (no real resumable token) — these could still trigger follow-up bootstrap skips on what is functionally a fresh session. @dsantoreis mentioned deferring to maintainer input. Would be good to have an explicit stance documented (even if the fix lands as a follow-up issue) so the behavior is predictable for affected backends.

Overall: LGTM on the implementation. The 3.4M token / ~$1.51 savings per 100-message session is meaningful for production setups.

@dsantoreis

This comment was marked as spam.

@dsantoreis

This comment was marked as spam.

@dsantoreis

This comment was marked as spam.

@darfaz

darfaz commented Mar 13, 2026

Copy link
Copy Markdown

LGTM — the synthetic CLI session ID concern from our earlier review is resolved. Commit 4c43df5ab correctly limits persistence to real resumable session IDs only. Clean fix, no further issues from our end. Thanks for the quick turnaround @dsantoreis.

@dsantoreis

This comment was marked as spam.

@aisle-research-bot

aisle-research-bot Bot commented Mar 13, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Workspace bootstrap guardrails can be skipped via contextInjection="first-message-only" (transcript-based)
2 🔵 Low Uncaught exception from FileHandle.close() in finally can crash run (DoS)

1. 🟡 Workspace bootstrap guardrails can be skipped via contextInjection="first-message-only" (transcript-based)

Property Value
Severity Medium
CWE CWE-657
Location src/agents/bootstrap-files.ts:116-131

Description

The new contextInjection: "first-message-only" mode can remove workspace bootstrap files (AGENTS.md, SOUL.md, USER.md, etc.) from the system prompt after any assistant message exists in the session transcript.

Why this is a security problem (depending on deployment/threat model):

  • Workspace bootstrap files are injected into the system prompt as # Project Context (see buildAgentSystemPrompt()), making them higher-priority than user messages.
  • Many installations (and the provided template) place security-relevant guardrails in these files (e.g., “don’t exfiltrate private data”, “don’t run destructive commands”, “don’t load MEMORY.md in shared contexts”).
  • With first-message-only, these guardrails are absent on subsequent turns, increasing the risk of prompt-injection success and accidental policy drift in long-running sessions.
  • The decision to skip bootstrap injection is based only on detecting an assistant role in the session JSONL tail. If an attacker can tamper with the session transcript file (multi-tenant/shared host, writable state dir, compromised user, etc.), they can pre-seed a fake assistant entry so bootstrap files are skipped even on the first real run.

Vulnerable logic:

if (mode === "first-message-only" && params.hasExistingAssistantMessages) {
  return [];
}

and transcript-based detection:

const parsed = JSON.parse(line);
if (parsed?.message?.role === "assistant") {
  return true;
}

Recommendation

Treat workspace bootstrap files as potentially security-relevant and avoid making their continued injection depend solely on transcript state.

Recommended mitigations (choose one or combine):

  1. Keep security-critical instructions out of user-editable bootstrap files and in a separate, always-injected system policy section.

  2. If you keep first-message-only, do not skip all files. For example, always inject a minimal allowlist of “guardrail” files (e.g., AGENTS.md / IDENTITY.md) while allowing persona/verbosity files to be skipped.

  3. Make skipping depend on a trusted session state flag (not on parsing user-writable JSONL). For example, persist a boolean like bootstrapInjectedOnce=true in the session store entry (sessions.json) that is written only by the agent runtime.

  4. Add a safety valve: if bootstrap files changed since last injection (hash/mtime), re-inject them.

Example (persist a trusted flag):

// pseudo-code: write to session store after first successful run
sessionEntry.bootstrapInjectedOnce = true;// later
if (contextInjection === "first-message-only" && sessionEntry.bootstrapInjectedOnce) {
  skipBootstrap();
}

2. 🔵 Uncaught exception from FileHandle.close() in finally can crash run (DoS)

Property Value
Severity Low
CWE CWE-248
Location src/agents/bootstrap-files.ts:61-63

Description

The sessionHasAssistantMessages helper attempts to be best-effort (it swallows errors from open/stat/read/JSON.parse), but the finally block awaits handle.close() without guarding errors.

  • Any rejection from FileHandle.close() (e.g., intermittent I/O error, invalidated FD, unusual FS behavior) will override the earlier catch-all and cause sessionHasAssistantMessages() to throw.
  • The caller (runEmbeddedAttempt) awaits this function without a surrounding try/catch, so the exception can abort the entire run, creating a denial-of-service / unexpected crash vector.

Vulnerable code:

} finally {
  await handle?.close();
}

Recommendation

Guard close() so it cannot throw out of the best-effort helper.

} finally {
  try {
    await handle?.close();
  } catch {// optionally log at debug level
  }
}

Optionally, if you want to preserve the error for logging without crashing:

} finally {
  await handle?.close().catch((err) => {
    warn?.(`session transcript close failed: ${String(err)}`);
  });
}

Analyzed PR: #40372 at commit 5cf4065

Last updated on: 2026-03-13T09:40:41Z

@dsantoreis

This comment was marked as spam.

@aisle-research-bot

aisle-research-bot Bot commented Mar 13, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Prompt-safety regression: optional config can skip workspace bootstrap policy files after first assistant message
2 🟡 Medium Cost/availability DoS via truncated session head scan in sessionHasAssistantMessages

1. 🟡 Prompt-safety regression: optional config can skip workspace bootstrap policy files after first assistant message

Property Value
Severity Medium
CWE CWE-693
Location src/agents/bootstrap-files.ts:111-120

Description

The new contextInjection: "first-message-only" mode can remove all workspace bootstrap files (AGENTS.md/SOUL.md/USER.md/etc.) from the system prompt after the first assistant message, which can weaken prompt-injection resistance and allow instruction/policy drift.

Why this matters:

  • Workspace bootstrap files are user/owner-controlled “policy/context” that OpenClaw injects into the system prompt (e.g., AGENTS.md template includes explicit security guidance like “Don’t exfiltrate private data. Ever.”).
  • With first-message-only, subsequent turns build a system prompt without those files, so the model no longer has the owner’s persistent constraints/operating rules unless they happen to remain in the conversation window.
  • This is a footgun: enabling the option to save tokens can make the agent more susceptible to gradual prompt injection (especially in long conversations / after compaction) and can cause the agent to take actions that the workspace policy would have prohibited.

Vulnerable logic (skips all bootstrap context files):

if (mode === "first-message-only" && params.hasExistingAssistantMessages) {
  return [];
}

Data-flow context:

  • Input: contextInjection comes from runtime params/config (params.config?.agents?.defaults?.contextInjection) in runEmbeddedAttempt.
  • Gate: sessionHasAssistantMessages(params.sessionFile) determines whether to skip.
  • Sink/impact: resolveBootstrapContextForRun() returns contextFiles=[], and the system prompt is built without the workspace bootstrap contents, removing owner-defined constraints from system-level instructions.

Notes:

  • Default remains "always", but the new option introduces an insecure configuration mode without an explicit security warning.

Recommendation

Do not allow first-message-only to drop all policy/context bootstrap files. Safer options:

  1. Always inject a minimal “security policy” subset (e.g., AGENTS.md “Red Lines” section, MEMORY handling rules) even when skipping the rest.

  2. Re-inject on high-risk turns (tool-use, external messaging, web fetch/search, filesystem writes) while allowing skipping on purely conversational turns.

  3. Add an explicit security warning in config help/docs when first-message-only is enabled.

Example: keep a minimal allowlist when skipping:

const MIN_SECURITY_FILES = new Set(["AGENTS.md", "SOUL.md"] as const);

if (mode === "first-message-only" && hasExistingAssistantMessages) {
  return params.files.filter(f => MIN_SECURITY_FILES.has(f.name));
}
return params.files;

This preserves persistent owner constraints while still reducing token usage.


2. 🟡 Cost/availability DoS via truncated session head scan in sessionHasAssistantMessages

Property Value
Severity Medium
CWE CWE-400
Location src/agents/bootstrap-files.ts:17-55

Description

The sessionHasAssistantMessages() helper only reads the first 16KB / 100 lines of a session JSONL file to determine whether the session already contains assistant messages.

Because Pi transcripts are JSONL with one JSON object per line, a single very large first user message line can push the first assistant message past the first 16KB chunk. In that case the function will:

  • read only the header + a partial first message line
  • fail JSON.parse() on the partial line (caught and ignored)
  • return false (no assistant found)

When contextInjection is set to "first-message-only", this false negative causes the system to repeatedly inject bootstrap context on subsequent turns, increasing prompt size and token spend. A malicious user can intentionally send an extremely large initial prompt to keep assistant messages out of the scanned head region, causing sustained token/cost amplification and potentially hitting model context limits (availability degradation).

Vulnerable code (truncation + per-line JSON.parse over a partial chunk):

const buf = Buffer.alloc(SESSION_HEAD_MAX_BYTES);
const { bytesRead } = await handle.read(buf, 0, buf.length, 0);
const chunk = buf.toString("utf-8", 0, bytesRead);
const lines = chunk.split(/\r?\n/).slice(0, SESSION_HEAD_MAX_LINES);
for (const line of lines) {
  ...
  try {
    const parsed = JSON.parse(line);
    if (parsed?.message?.role === "assistant") {
      return true;
    }
  } catch {// skip malformed lines
  }
}

Recommendation

Make assistant-history detection robust against large first lines / head truncation.

Good options (pick one):

  1. Scan from the tail (recommended): read the last N bytes (e.g., 64–256KB), split into lines, skip the first partial line (since you may start mid-line), then JSON-parse each complete line and look for type:"message" + message.role=="assistant".

  2. Use streaming line reading with limits: createReadStream() + readline.createInterface(), but stop after finding an assistant. If you want to avoid reading huge early lines, set a maximum scanned byte budget and/or move to tail scanning.

  3. Persist a small sidecar/metadata flag in the session store (e.g., hasAssistant: true once an assistant message is appended) rather than parsing the transcript file.

Example (tail scan):

import fs from "node:fs/promises";

const TAIL_MAX_BYTES = 256 * 1024;

export async function sessionHasAssistantMessages(sessionFile: string): Promise<boolean> {
  let handle: fs.FileHandle | undefined;
  try {
    handle = await fs.open(sessionFile, "r");
    const stat = await handle.stat();
    const start = Math.max(0, stat.size - TAIL_MAX_BYTES);
    const len = stat.size - start;
    const buf = Buffer.alloc(len);
    const { bytesRead } = await handle.read(buf, 0, len, start);
    const chunk = buf.toString("utf-8", 0, bytesRead);

    const lines = chunk.split(/\r?\n/);// If we started mid-file, drop the first possibly-partial line
    if (start > 0) lines.shift();

    for (const line of lines) {
      if (!line.trim()) continue;
      try {
        const parsed = JSON.parse(line);
        if (parsed?.type === "message" && parsed?.message?.role === "assistant") return true;
      } catch {
        continue;
      }
    }
  } catch {
    return false;
  } finally {
    try { await handle?.close(); } catch { /* ignore */ }
  }
  return false;
}

This avoids the “huge first line keeps assistant messages out of the scanned head” false negative and reduces the risk of token/cost amplification when contextInjection: "first-message-only" is enabled.


Analyzed PR: #40372 at commit 34cbb95

Last updated on: 2026-03-13T08:13:26Z

@dsantoreis

This comment was marked as spam.

@aisle-research-bot

aisle-research-bot Bot commented Mar 13, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Context injection policy bypass via truncated head-only scan of session transcript
2 🔵 Low Extra bootstrap hook execution + bootstrap file loading when contextInjection is meant to skip context (metadata-only path)

1. 🟡 Context injection policy bypass via truncated head-only scan of session transcript

Property Value
Severity Medium
CWE CWE-697
Location src/agents/bootstrap-files.ts:17-55

Description

The new sessionHasAssistantMessages() helper reads only the first 16KB / 100 lines of the session JSONL transcript to decide whether an assistant message exists. This decision is used to enforce contextInjection: "first-message-only" (skip injecting AGENTS.md/SOUL.md/etc after the assistant has spoken).

Because it only parses the file head, a user can cause persistent false negatives ("no assistant history") even when assistant messages exist, by ensuring the first assistant message is not visible within that head window:

  • Single huge first user message: if the first user JSONL line is >16KB, the first read chunk ends mid-line; JSON parsing fails and the assistant message (which starts after that long line) is never seen.
  • Many user messages before first assistant reply (queue/backlog/import/replay scenarios): if >100 user-message lines occur before the first assistant message line, the assistant role is never observed.
  • Long assistant/tool blocks early can similarly push the first assistant record beyond the byte cap.

Security/robustness impact when contextInjection is configured to first-message-only:

  • The agent will continue re-injecting mutable workspace bootstrap files on later turns, defeating the intended behavior.
  • If an untrusted user can get the agent to modify AGENTS.md/SOUL.md mid-session, those modified instructions may continue to be injected into the system prompt on subsequent turns (policy bypass / prompt-injection surface), and it can also cause repeated token/cost overhead.

Vulnerable code (head-only parsing + silent failure):

const buf = Buffer.alloc(SESSION_HEAD_MAX_BYTES);
const { bytesRead } = await handle.read(buf, 0, buf.length, 0);
const chunk = buf.toString("utf-8", 0, bytesRead);
const lines = chunk.split(/\r?\n/).slice(0, SESSION_HEAD_MAX_LINES);
for (const line of lines) {
  try {
    const parsed = JSON.parse(line);
    if (parsed?.message?.role === "assistant") {
      return true;
    }
  } catch {// skip malformed lines
  }
}

Relevant locations:

  • src/agents/bootstrap-files.ts [sessionHasAssistantMessages lines 17-55], and its use in injection gating via applyContextInjectionFilter [lines 106-121]
  • src/agents/pi-embedded-runner/run/attempt.ts uses the result to decide injection behavior (diff hunk around the new hasExistingAssistantMessages assignment)

Recommendation

Make assistant-history detection robust against long/partial lines and large prefixes.

Recommended approaches (pick one):

  1. Stream parse JSONL line-by-line with a maximum total bytes/lines, but do not stop at an arbitrary byte offset mid-line:
import fs from "node:fs";
import readline from "node:readline";

const MAX_LINES = 2000;
const MAX_TOTAL_BYTES = 2 * 1024 * 1024; // cap
const MAX_LINE_BYTES = 256 * 1024; // cap per record

export async function sessionHasAssistantMessages(sessionFile: string): Promise<boolean> {
  const stream = fs.createReadStream(sessionFile, { encoding: "utf8" });
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
  let lines = 0;
  let totalBytes = 0;

  try {
    for await (const line of rl) {
      lines++;
      totalBytes += Buffer.byteLength(line, "utf8") + 1;
      if (lines > MAX_LINES || totalBytes > MAX_TOTAL_BYTES) break;
      if (Buffer.byteLength(line, "utf8") > MAX_LINE_BYTES) continue; // skip huge record but keep scanning

      if (!line.trim()) continue;
      try {
        const parsed = JSON.parse(line);
        if (parsed?.type === "message" && parsed?.message?.role === "assistant") return true;
      } catch {// skip malformed line
      }
    }
  } finally {
    rl.close();
    stream.destroy();
  }
  return false;
}
  1. Alternatively, scan the tail (last N lines/bytes) as well as the head, ensuring you start parsing on newline boundaries, so long initial user records cannot hide assistant messages.

Also consider tightening the predicate to parsed.type === "message" && parsed.message.role === "assistant" to avoid accidentally matching non-message records that might contain a message field.


2. 🔵 Extra bootstrap hook execution + bootstrap file loading when contextInjection is meant to skip context (metadata-only path)

Property Value
Severity Low
CWE CWE-400
Location src/agents/pi-embedded-runner/run/attempt.ts:838-851

Description

In runEmbeddedAttempt, when contextInjection === "first-message-only" and the session already has assistant messages, the code intentionally skips injecting bootstrap context via resolveBootstrapContextForRun(...).

However, it then performs a second call to resolveBootstrapFilesForRun with contextInjection: "always" purely to compute workspaceNotes.

Impact:

  • Unexpected extra internal hook execution: resolveBootstrapFilesForRun always invokes applyBootstrapHookOverrides(), which triggers the agent:bootstrap internal hook event. Hook handlers are arbitrary JS/TS code (often workspace-provided) and can perform expensive work or side effects (network/FS).
  • DoS/perf risk: This adds an additional hook dispatch per message in this mode, and may force full bootstrap file materialization (including file contents) even though context injection was skipped for token/perf reasons.
  • Breaks “skip context” expectations: Even though context files are not injected into the model prompt, this path still loads them and passes them through the hook pipeline, re-introducing side effects and potential sensitive-data handling by hooks.

Vulnerable code (2nd hook-triggering call in skipped mode):

const bootstrapMetadataFiles =
  contextInjection === "first-message-only" && hasExistingAssistantMessages
    ? await resolveBootstrapFilesForRun({
        ...,
        contextInjection: "always",
        hasExistingAssistantMessages: false,
      })
    : hookAdjustedBootstrapFiles;

Relevant behavior: resolveBootstrapFilesForRun unconditionally triggers the internal hook event:

  • applyBootstrapHookOverrides(...)triggerInternalHook(event)

This makes the metadata-only check a hook execution sink.

Recommendation

Avoid running the full bootstrap resolution + hook pipeline just to detect whether BOOTSTRAP.md exists.

Mitigations (choose one):

  1. Direct filesystem existence check (no hooks, no file content load):
import fs from "node:fs/promises";
import path from "node:path";

const hasBootstrap = await fs
  .access(path.join(effectiveWorkspace, "BOOTSTRAP.md"))
  .then(() => true)
  .catch(() => false);
  1. Add a metadata-only API (e.g., resolveBootstrapFileMetadataForRun) that returns {name, missing} without reading file contents and without calling applyBootstrapHookOverrides.

  2. If hook overrides are required, add a flag to resolveBootstrapFilesForRun like skipHooks: true and use it for metadata-only checks.

Also consider caching by sessionId when sessionKey is absent to avoid repeated reads during long sessions.


Analyzed PR: #40372 at commit 34cbb95

Last updated on: 2026-03-13T08:45:38Z

Co-authored-by: Daniel <dsantoreis@gmail.com>
@dsantoreis

This comment was marked as spam.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2648d3788d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +42 to +43
if (start > 0) {
lines.shift();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep boundary line when scanning transcript tail

When the session file exceeds the 256KB tail window and start lands exactly on a JSONL line boundary, the unconditional lines.shift() drops a complete first record instead of only trimming a partial line. If that dropped record is the only assistant entry in the scanned region, sessionHasAssistantMessages incorrectly returns false, so contextInjection: "first-message-only" re-injects bootstrap files on follow-up turns and loses the expected token/privacy reduction.

Useful? React with 👍 / 👎.

This comment was marked as spam.

Co-authored-by: Daniel <dsantoreis@gmail.com>
@dsantoreis

This comment was marked as spam.

@openclaw-barnacle openclaw-barnacle Bot added the r: too-many-prs Auto-close: author has more than twenty active PRs. label Mar 13, 2026
@openclaw-barnacle

Copy link
Copy Markdown

Closing this PR because the author has more than 10 active PRs in this repo. Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5cf4065e17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +157 to +161
const bootstrapFiles = applyContextInjectionFilter({
files: modeFiltered,
contextInjection: params.contextInjection,
hasExistingAssistantMessages: params.hasExistingAssistantMessages ?? false,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep bootstrap metadata when skipping file injection

In first-message-only mode with prior assistant history, this now replaces bootstrapFiles with the filtered (often empty) list before returning from resolveBootstrapFilesForRun. That means downstream consumers in runEmbeddedAttempt receive no bootstrap metadata at all on follow-up turns, so checks like workspaceNotes (AGENTS.md presence) and bootstrap reporting/truncation metadata are silently lost even though the workspace files still exist. Keep an unfiltered metadata list and apply context-injection filtering only to the list used for prompt contextFiles.

Useful? React with 👍 / 👎.

@dsantoreis

This comment was marked as spam.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling r: too-many-prs Auto-close: author has more than twenty active PRs. size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance: Workspace file injection wastes 93.5% of token budget

2 participants