Skip to content

Bridge Codex native hooks into OpenClaw#71008

Merged
steipete merged 8 commits intomainfrom
codex/native-hook-relay-foundation
Apr 24, 2026
Merged

Bridge Codex native hooks into OpenClaw#71008
steipete merged 8 commits intomainfrom
codex/native-hook-relay-foundation

Conversation

@pashpashpash
Copy link
Copy Markdown
Contributor

@pashpashpash pashpashpash commented Apr 24, 2026

OpenClaw’s Codex path now has the native hook bridge we were building toward, not just the relay foundation. The important split is still the same: OpenClaw dynamic tools are already executed by OpenClaw, while Codex-native tools like shell and apply patch run inside Codex. This PR gives those Codex-native tool events a controlled way back into OpenClaw’s plugin and approval surfaces.

The core relay stays provider-neutral. A native harness hook command can call openclaw hooks relay, identify the active relay/session/run, and send a normalized pre_tool_use, post_tool_use, or permission_request event back through the authenticated gateway. Codex is the first adapter, but the relay shape is deliberately not Codex-only, so a future Claude Code adapter can reuse the same registry, gateway method, CLI command, and failure behavior.

For Codex, the app-server adapter now injects per-thread hook config on both thread/start and thread/resume. It enables only PreToolUse, PostToolUse, and PermissionRequest, uses Codex config overrides directly, and does not write user or global hook files. The relay is registered for the turn, cleaned up on success, failure, or abort, and scoped by provider, relay id, session id, session key, run id, allowed events, and expiry.

The behavior now maps into the v1 contract:

  • PreToolUse calls OpenClaw before_tool_call for Codex-native tools. Plugins can block; returned argument rewrites are intentionally ignored because Codex hooks do not support safe argument mutation yet.
  • PostToolUse calls OpenClaw after_tool_call with the native tool input and response, giving plugins an observation path for Codex-native results.
  • PermissionRequest goes through OpenClaw approval policy. An allow or deny decision is rendered back into Codex hook JSON; if OpenClaw cannot decide, the relay defers so Codex’s normal permission path can continue.

The concrete user story this fixes is a security plugin like Knostic Shield. Before this PR, openclaw-shield could register its L3 before_tool_call blocker, but Codex-native Bash calls could slip past that plugin surface because the shell execution happened inside Codex. I cloned the real knostic/openclaw-shield plugin, loaded its actual register(api) implementation into OpenClaw’s hook runner, and sent the relay a Codex-shaped PreToolUse payload for Bash with rm -rf /tmp/openclaw-shield-smoke-target. The plugin registered before_tool_call at priority 200, the relay returned Codex-compatible JSON with permissionDecision: "deny", and the reason contained Knostic’s destructive-command block message. The same smoke sent a harmless printf command and got the expected empty stdout/stderr with exit code 0, meaning Codex would proceed normally.

There are still deliberate v1 boundaries. This does not mutate native tool arguments, does not rewrite Codex’s native thread records, does not make tool_result_persist apply to Codex-native tool records, and does not expose Stop/final-answer gating. Those are separate product decisions, not accidental gaps in this bridge.

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Apr 24, 2026

🔒 Aisle Security Analysis

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

# Severity Title
1 🟠 High Memory exhaustion risk: native hook relay stores unbounded rawPayload in global invocation buffer
2 🟡 Medium Native hook relay uses relayId as bearer token and exposes it via command-line arguments
3 🟡 Medium Codex native tool hook relay ignores adjusted params from before_tool_call hooks (sanitization bypass)
4 🟡 Medium Approval spoofing via unvalidated pluginId in plugin.approval.request (native hook relay uses synthetic ID)
1. 🟠 Memory exhaustion risk: native hook relay stores unbounded rawPayload in global invocation buffer
Property Value
Severity High
CWE CWE-400
Location src/agents/harness/native-hook-relay.ts:323-328

Description

The native hook relay records the entire rawPayload for every invocation into a global in-memory invocations array.

  • Input: rawPayload is provided by gateway/CLI callers (nativeHook.invoke).
  • Validation: isJsonValue() bounds depth/nodes, but does not bound string sizes / total serialized size.
  • Sink: recordNativeHookRelayInvocation() stores the full payload (and metadata) for up to MAX_NATIVE_HOOK_RELAY_INVOCATIONS (200) entries.
  • Impact: A caller can send near-maximum gateway frames (gateway WS policy advertises MAX_PAYLOAD_BYTES = 25MB) repeatedly, causing the server to retain large payloads in memory (potentially multiple GB), leading to process memory pressure / OOM and denial of service. The buffer also increases the blast radius of accidental secret inclusion in hook payloads (retained in memory for later crash dumps/diagnostics).

Vulnerable code:

function recordNativeHookRelayInvocation(invocation: NativeHookRelayInvocation): void {
  invocations.push(invocation);
  if (invocations.length > MAX_NATIVE_HOOK_RELAY_INVOCATIONS) {
    invocations.splice(0, invocations.length - MAX_NATIVE_HOOK_RELAY_INVOCATIONS);
  }
}

Recommendation

Avoid retaining full, unbounded payloads in memory.

Recommended mitigations (pick one or combine):

  1. Store only a bounded preview (and/or a hash), not the full payload.
  2. Enforce a max byte size for rawPayload before recording (e.g., by measuring JSON.stringify(rawPayload) size and truncating/removing large fields).
  3. Track a total-bytes budget across the invocations ring buffer and evict oldest entries until under budget.

Example: size-bound storage with redaction/truncation:

const MAX_INVOCATION_PAYLOAD_BYTES = 256 * 1024; // 256KB

function safePayloadForRecord(raw: JsonValue): JsonValue {
  const text = JSON.stringify(raw);
  if (Buffer.byteLength(text, "utf8") <= MAX_INVOCATION_PAYLOAD_BYTES) return raw;
  return {
    __truncated: true,
    __bytes: Buffer.byteLength(text, "utf8"),
    preview: text.slice(0, 4096),
  } as unknown as JsonValue;
}

function recordNativeHookRelayInvocation(invocation: NativeHookRelayInvocation): void {
  invocations.push({ ...invocation, rawPayload: safePayloadForRecord(invocation.rawPayload) });// existing ring buffer logic...
}

Also consider not storing tool inputs/responses by default (or redacting known-sensitive keys) unless an explicit debug flag is enabled.

2. 🟡 Native hook relay uses relayId as bearer token and exposes it via command-line arguments
Property Value
Severity Medium
CWE CWE-284
Location src/agents/harness/native-hook-relay.ts:237-303

Description

The native hook relay authentication/authorization relies on possession of a relayId (UUID) stored in an in-memory map. The relayId is passed on the command line to the openclaw hooks relay subprocess, which can be observable via process listings, crash reports, telemetry, or logs depending on the environment.

Impact if relayId is obtained by an attacker who can reach the gateway method nativeHook.invoke:

  • The attacker can spoof native hook events (pre_tool_use, post_tool_use, permission_request) for the victim relay
  • This can trigger plugin hooks and/or generate permission approval prompts using attacker-controlled rawPayload
  • invokeNativeHookRelay does not require any additional binding (e.g., sessionKey/runId MAC, per-invocation nonce) beyond relayId + provider + allowed event

Vulnerable code (relayId embedded in command; invocation authorized by relayId lookup):

// command includes --relay-id <relayId>
return shellQuoteArgs([
  ...argv,
  "hooks",
  "relay",
  "--provider",
  params.provider,
  "--relay-id",
  params.relayId,
  "--event",
  params.event,
]);// invocation authorizes by relayId only
const relayId = readNonEmptyString(params.relayId, "relayId");
const registration = relays.get(relayId);

Recommendation

Avoid treating relayId as a long-lived bearer secret exposed to other processes.

Recommended mitigations (use one or combine):

  1. Do not pass secrets via argv: pass the relay credential via stdin, an inherited FD, or an environment variable that is not logged, and ensure it is cleared.

  2. Bind invocations to a second factor (defense-in-depth): require a MAC/HMAC over (relayId, event, timestamp/nonce, runId, sessionId) using a secret known only to the harness + gateway. Verify on the gateway before accepting the invocation.

  3. Use one-time / short-lived tokens: issue per-event or per-invocation nonces stored server-side and invalidate after use.

Example sketch (HMAC-bound token):

// when creating command/config
const nonce = randomUUID();
const mac = hmacSha256(sessionKeySecret, `${relayId}:${runId}:${event}:${nonce}`);// pass nonce+mac (not relayId alone) to CLI// on invoke
verifyHmac(sessionKeySecret, `${relayId}:${runId}:${event}:${nonce}`, mac);
assertAndConsumeNonce(relayId, nonce);
3. 🟡 Codex native tool hook relay ignores adjusted params from before_tool_call hooks (sanitization bypass)
Property Value
Severity Medium
CWE CWE-693
Location src/agents/harness/native-hook-relay.ts:366-386

Description

The Codex native hook relay forwards Codex PreToolUse events into OpenClaw's before_tool_call hook, but drops any parameter mutations returned by plugins.

  • Input: Codex-native tool arguments are parsed from rawPayload.tool_input.
  • Policy hook: runBeforeToolCallHook() is invoked and can return { blocked: false, params: <adjusted> } when plugins sanitize or rewrite arguments.
  • Bypass: For Codex-native tools the relay only honors outcome.blocked; it always returns a noop to Codex when allowed, meaning Codex executes the original (potentially unsafe) arguments.

This creates a gap where existing or future security plugins that rely on argument rewriting (e.g., stripping dangerous shell flags, forcing safe working directories, removing network destinations) may silently become ineffective for Codex-native tool executions, potentially allowing unsafe tool invocations that would otherwise have been mitigated.

Vulnerable code:

const outcome = await runBeforeToolCallHook({ toolName, params: toolInput, ... });
if (outcome.blocked) {
  return params.adapter.renderPreToolUseBlockResponse(outcome.reason);
}// ... intentionally ignore adjusted params
return params.adapter.renderNoopResponse(params.invocation.event);

Recommendation

Avoid silently ignoring adjusted params for Codex-native tools.

Options (pick one):

  1. Fail closed when a plugin returns adjusted params (prevents a false sense of security):
if (!outcome.blocked && outcome.params && !deepEqual(outcome.params, toolInput)) {
  return params.adapter.renderPreToolUseBlockResponse(
    "Tool blocked: parameter mutation requested but not supported for Codex-native tools"
  );
}
  1. Surface a clear warning/diagnostic event whenever outcome.params differs, so operators can detect ineffective sanitization.
  2. Implement argument mutation support by emitting Codex's supported mechanism (if/when available) or by routing Codex-native tool execution through OpenClaw tooling when sanitization is required.

Also update plugin-facing docs/contracts to explicitly state that parameter rewrites are not enforced for Codex-native tool calls and should use blocking/approval instead.

4. 🟡 Approval spoofing via unvalidated `pluginId` in `plugin.approval.request` (native hook relay uses synthetic ID)
Property Value
Severity Medium
CWE CWE-290
Location src/agents/harness/native-hook-relay.ts:594-606

Description

The native hook relay permission flow creates approvals via plugin.approval.request and sets a synthetic pluginId string:

  • native-hook-relay.ts sends pluginId: openclaw-native-hook-relay-${provider} when requesting user approval.
  • On the server side, plugin.approval.request accepts the caller-provided pluginId as-is and does not validate that it corresponds to an installed/authorized plugin identity.

This enables approval identity spoofing / UI confusion: any component able to call plugin.approval.request can set pluginId to openclaw-native-hook-relay-codex (or any other plugin ID) and craft titles/descriptions that appear to come from the trusted native hook relay, potentially tricking users into approving actions.

Vulnerable code (synthetic ID used for security-sensitive approval UI):

pluginId: `openclaw-native-hook-relay-${request.provider}`,

Recommendation

Bind approval identity to an authenticated/authorized principal rather than caller-controlled strings.

Options:

  1. Server-side enforcement: in plugin.approval.request, reject/override pluginId unless it matches the authenticated plugin context of the caller.

  2. Separate identity fields: introduce a dedicated source/requesterType field (e.g., "native_hook_relay") and keep pluginId as null for non-plugin requesters.

Example (conceptual):

// native hook relay should not claim to be a plugin
await callGatewayTool("plugin.approval.request", opts, {
  pluginId: null,
  requesterType: "native_hook_relay",
  requesterId: `native-hook-relay:${request.provider}`,
  title,
  description,
  ...
});

And on the gateway handler:

if (params.pluginId != null) {// verify params.pluginId belongs to the authenticated plugin principal
  assertCallerPluginIdMatches(params.pluginId, context);
}

Analyzed PR: #71008 at commit d49b63a

Last updated on: 2026-04-24T15:36:50Z

@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime cli CLI command changes agents Agent runtime and tooling extensions: codex size: XL maintainer Maintainer-authored PR labels Apr 24, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR adds a provider-neutral native harness hook relay foundation: an in-process relay registry, a hidden openclaw hooks relay CLI command that reads provider hook JSON from stdin and forwards it through the authenticated gateway, and a Codex-specific adapter that injects per-thread config overrides (PreToolUse, PostToolUse, PermissionRequest) on thread/start and thread/resume.

  • Unbounded invocations array (src/agents/harness/native-hook-relay.ts): The module-level invocations array grows indefinitely — each gateway call appends a NativeHookRelayInvocation (including the full rawPayload) and unregisterNativeHookRelay does not remove the associated entries. For a long-running process processing many agent runs with frequent tool use, this is a steady memory leak. A retention strategy (e.g. ring-buffer, drain-on-unregister) should be in place before the invocations data is useful for follow-up PRs anyway.

Confidence Score: 4/5

Safe to merge after addressing the unbounded invocations array; all other infrastructure is well-structured and tested.

Two P1 comments both point at the same root cause: the module-level invocations array has no eviction and unregister doesn't prune it, causing memory growth in production. All other aspects — validation, error handling, relay lifecycle management across three exit paths, gateway scope assignment, and Codex config building — are implemented correctly and covered by tests.

src/agents/harness/native-hook-relay.ts — the invocations array and unregisterNativeHookRelay need a retention/cleanup strategy before merge.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/harness/native-hook-relay.ts
Line: 98

Comment:
**Unbounded in-memory invocations array — memory leak**

`invocations` accumulates every hook call for the lifetime of the process and is never pruned. `unregisterNativeHookRelay` only removes the relay from `relays`; the corresponding entries stay in `invocations` forever. Each entry retains the full `rawPayload` (arbitrary JSON that can include tool outputs). A long-running OpenClaw process handling many agent sessions will grow unboundedly here.

Since the PR description explicitly notes invocations are kept for "later mapping" in follow-up PRs, even a simple cap (e.g. a ring-buffer of the last N entries, or draining entries when a relay is unregistered) would prevent the leak from landing on main.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/harness/native-hook-relay.ts
Line: 144-146

Comment:
**`unregister` does not clean up associated invocations**

`unregisterNativeHookRelay` removes the relay registration from `relays` but leaves all invocations it produced sitting in the `invocations` array. After a Codex run completes and the relay is unregistered, the run's tool-use payloads continue to occupy memory indefinitely. This is the companion to the unbounded-array concern above — fixing either one addresses both.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Add native harness hook relay foundation" | Re-trigger Greptile

Comment thread src/agents/harness/native-hook-relay.ts
Comment thread src/agents/harness/native-hook-relay.ts
Copy link
Copy Markdown

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

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: 7d2eaeb921

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/agents/harness/native-hook-relay.ts Outdated
Comment thread extensions/codex/src/app-server/run-attempt.ts
@pashpashpash pashpashpash changed the title Add native harness hook relay foundation Bridge Codex native hooks into OpenClaw Apr 24, 2026
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label Apr 24, 2026
@steipete steipete force-pushed the codex/native-hook-relay-foundation branch from 43a9a24 to b98bba6 Compare April 24, 2026 15:22
@openclaw-barnacle openclaw-barnacle Bot added the scripts Repository scripts label Apr 24, 2026
@steipete steipete force-pushed the codex/native-hook-relay-foundation branch from 7b47724 to d49b63a Compare April 24, 2026 15:33
@steipete steipete merged commit 7a958d9 into main Apr 24, 2026
68 checks passed
@steipete steipete deleted the codex/native-hook-relay-foundation branch April 24, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling cli CLI command changes docs Improvements or additions to documentation extensions: codex gateway Gateway runtime maintainer Maintainer-authored PR plugin: google-meet scripts Repository scripts size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants