Skip to content

feat: add OpenAI Codex platform support#13

Merged
ashwin-corridor merged 19 commits into
mainfrom
cursor/add-codex-support-b167
May 18, 2026
Merged

feat: add OpenAI Codex platform support#13
ashwin-corridor merged 19 commits into
mainfrom
cursor/add-codex-support-b167

Conversation

@ashwin-corridor

@ashwin-corridor ashwin-corridor commented May 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds first-class support for OpenAI Codex hooks to hookshot. Codex hooks use the same JSON wire format as Claude Code hooks, so the implementation re-uses the existing claude.* types while exposing the Codex-specific differences (the apply_patch tool with its unified-diff envelope, the mcp__* matcher convention, the codex_hooks / hooks feature flag, and the Ask/updatedInput/suppressOutput fail-closed quirks) where they matter.

Also fixes OnPromptSubmit for Claude Code and Droid: ctx.Cwd was previously empty for both platforms because the bridge wasn't forwarding input.Cwd into the PromptContext. Codex was already correct; this PR aligns Claude and Droid.

What's new

Bug fixes

  • OnPromptSubmit now populates ctx.Cwd for Claude Code and Droid. Both platforms send cwd in the hook stdin, but the claude-user-prompt-submit and droid-user-prompt-submit handlers were constructing PromptContext without forwarding it. This meant callers had to fall back to os.Getwd() or CLAUDE_PROJECT_DIR heuristics — neither of which is reliable when hooks are launched as subprocesses by a plugin from a cache directory. The codex-user-prompt-submit handler already forwarded Cwd correctly; the two missing cases are fixed here.
  • PreToolUse fail-closed quirks on CodexupdatedInput and suppressOutput are rejected by the Codex runtime with unsupported <field> errors (the upstream doc currently claims fail-open; runtime behavior is fail-closed). The codex package's helper output now omits both fields and the unified bridge routes through the codex package's PostTool* helpers so this stays correct without per-caller workarounds.
  • AskExecution is rewritten to Deny on Codex. Codex parses but does not enforce permissionDecision: "ask" today, so an Ask decision would silently fail open. The unified bridge fails closed; on every other platform Ask continues to surface an approval prompt.
  • Installer matcher includes Bash for both PreToolUse and PostToolUse (Bash|apply_patch|mcp__.*). The old PostToolUse matcher (apply_patch|mcp__.*) never matches anything Codex 0.130.0+ actually emits for file edits.

Unified API

  • New PlatformCodex constant in unified.go.
  • Every unified handler now also registers a codex-* entry:
    • OnStopcodex-stop
    • OnBeforeExecutioncodex-pre-tool-use (classifies Bash, mcp__*, and apply_patch accordingly; surfaces apply_patch's patch text via ctx.Command)
    • OnAfterFileEditcodex-post-tool-use (handles all four file-edit shapes — see "Bash bridge" below)
    • OnPromptSubmitcodex-user-prompt-submit
  • StopContext.ShouldSkip() honors StopHookActive for Codex (same as Claude Code / Droid).
  • FileEditContext gains a NewFilePath field populated for rename operations. For Codex apply_patch *** Move to:, the unified bridge invokes the handler twice — once with FilePath set to the source and once with FilePath set to the destination — and populates NewFilePath on both invocations. This means a FilePath-only allowlist that permits the benign source still receives a separate event for the destination so it can deny moves to sensitive paths like ../../.ssh/authorized_keys. Handlers that want to detect a rename should check ctx.NewFilePath != "" && ctx.NewFilePath != ctx.FilePath.

Codex Bash bridge (the production fix)

codex-post-tool-use now handles all four shapes Codex emits for file operations and reduces them to the same OnAfterFileEdit pipeline:

tool_name tool_input.command shape Parser Source
Write/Edit (native fields) inline rare; Codex aliases for apply_patch
apply_patch unified-diff envelope codex.ParseApplyPatch uncommon — Codex usually routes via Bash
Bash (edit) apply_patch <<'PATCH' … *** End Patch … PATCH codex.ParseApplyPatchFromBash how Codex routes edits to existing files
Bash (write) cat <<'EOF' > FILE … EOF (and tee) NEW codex.ParseBashRedirectWrite how Codex routes greenfield writes

ParseBashRedirectWrite handles:

  • quoted (<<'EOF', <<"EOF") and unquoted (<<EOF) heredoc delimiters,
  • <<- tab-stripping,
  • > (overwrite) and >> (append) redirects,
  • cat … and tee [-a] … invocations,
  • cd … && cat … cwd-prefixed forms.

Paths are preserved verbatim so downstream policies can reject suspicious paths (e.g. ../../etc/passwd) themselves. The parser bails out (ok=false) on missing heredoc terminator rather than guessing — a well-formed Codex command always terminates the heredoc.

For Codex apply_patch envelopes (cases 2 & 3) we run them through codex.ParseApplyPatch and invoke the user's handler exactly once per file. For greenfield writes (case 4) we synthesize a single FileEdit{OldString: "", NewString: body} so the same dispatchPatch reducer drives every shape. Per-file decisions reduce as: any FileEditBlock wins (reasons concatenated), otherwise context strings are concatenated.

Platform package

  • New codex/ package (types.go, helpers.go, doc.go, apply_patch.go, bash_write.go, helpers_test.go) that re-exports the relevant claude types and helpers with Codex-flavored documentation, plus the parsers used by the unified bridge.

Installer

  • hookshot install --codex --binary … writes ~/.codex/hooks.json with matchers that include Bash|apply_patch|mcp__.* for both PreToolUse and PostToolUse so the bridge actually sees the events.
  • The installer reminds the user that the [features].hooks = true feature flag in ~/.codex/config.toml must be enabled separately (the legacy codex_hooks = true alias only loads the legacy hook subset on 0.130.0+).
  • Running hookshot install without flags now installs to Codex as well.

Docs

  • New docs/reference-codex.md covering every Codex event, the helpers, the fail-closed fields, the AskDeny rewrite, the apply_patch parsing semantics (including the rename double-invocation), the Bash bridge, and why the matcher needs Bash|apply_patch|mcp__.*.
  • README.md, doc.go, hookshot.go, docs/reference-unified.md, codex/doc.go, and examples/multi-hook updated to reference the new bridge, the new codex-* handler names, the updated matchers, and the new NewFilePath field.

Tests

  • codex/bash_write_test.go — 13 unit tests covering every variant of cat/tee heredoc writes (quoted/unquoted/<<- delimiters, append, cd-prefix, empty-line bodies, missing-terminator bail-out, fake-delimiter-in-body, path-traversal preservation).
  • codex/apply_patch_test.go — parser tests covering Add, Update, Delete, multi-hunk Update, multi-file patches, Move to: renames, leading here-doc wrappers, malformed input, and Bash-heredoc envelope detection (with the apply_patch invocation regex requiring an actual << operator to avoid false positives on filenames or doc strings).
  • unified_test.go — integration tests for the unified bridge:
    • TestStopContext_ShouldSkip_Codex + expanded TestPlatformConstants.
    • TestCodexPreToolUse_AskFailsClosed — Ask decision must emit permissionDecision: "deny", not "ask".
    • TestCodexPreToolUse_ApplyPatchPopulatesCommandapply_patch tool_input.command flows through to ExecutionContext.Command.
    • TestCodexPreToolUse_MCPClassifiedAsMCP — MCP tool names hit the handler as ExecutionMCP.
    • TestCodexPostToolUse_ApplyPatchParsesFilesAndEdits — multi-file Add/Update/Delete patch invokes the handler once per file with the right FilePath and Edits.
    • TestCodexPostToolUse_ApplyPatchMoveToInvokesHandlerForDestination*** Move to: invokes the handler twice (source + destination) and populates NewFilePath on both.
    • TestCodexPostToolUse_ApplyPatchMoveToBlockOnDestinationPropagates — a FilePath-only policy blocking .ssh/authorized_keys correctly blocks even when the source path is benign.
    • TestCodexPostToolUse_BashApplyPatchHeredocDispatchesapply_patch <<'PATCH' shape on Bash routes through OnAfterFileEdit identically to the native path.
    • TestCodexPostToolUse_BashApplyPatchAbsolutePathBinaryDispatches — per-session shim path prefix still recognized.
    • TestCodexPostToolUse_BashNonApplyPatchCommandSkipped — ordinary Bash commands don't trigger file-edit handler.
    • TestCodexPostToolUse_BashCatHeredocWriteDispatchescat <<'EOF' > FILE … EOF greenfield writes now route through OnAfterFileEdit with the correct FilePath + synthesized Edits.
    • TestCodexPostToolUse_BashCatHeredocBlockPropagates — a FilePath-suffix-based block policy (*.env) correctly results in PostToolBlock for cat-heredoc writes.
    • TestCodexPostToolUse_BashApplyPatchBlockPropagates — any per-file FileEditBlock results in a PostToolBlock with the combined reason.

Verification

go vet ./...
go build ./...
go test ./...

Plus an end-to-end check via the probe harness: asking codex exec to create a new file now produces an afterFileEdit event with the correct file path and body (previously zero events; see commit 8378bdf body for the captured trace).

Notes

  • apply_patch is classified as ExecutionTool (not Shell or MCP) in the unified OnBeforeExecution, so handlers can detect it via ctx.ToolName == "apply_patch". The patch text is also exposed via ctx.Command so policies can inspect it without re-parsing ToolInput.
  • Codex carries a few fields not represented in the shared BaseInput (model, turn_id, last_assistant_message). They remain accessible via hookshot.ReadRawInput and are documented in docs/reference-codex.md.
  • The installer does not flip the [features].hooks feature flag — that's still the user's responsibility, and the installer prints a reminder. On Codex 0.130.0+ the deprecated codex_hooks = true alias does NOT enable shell-tool routing for Bash PreToolUse/PostToolUse, so users with that flag set will only see SessionStart/UserPromptSubmit/Stop + MCP tool calls fire.
  • codex.Ask (the platform-level helper) still emits "ask" for forward-compat / round-trip testing; only the unified OnBeforeExecution bridge rewrites it to Deny.
  • FileEditContext.NewFilePath is empty for in-place edits and on every non-Codex platform; this addition is strictly additive for existing handlers.
  • ParseBashRedirectWrite does not currently cover echo … > FILE or printf … > FILE shapes — those are uncommon in Codex's output (it prefers heredocs for any multi-line content) and would require a slightly different parser. Easy to add as a follow-up if real sessions surface them.

Codex hooks use the same JSON wire format as Claude Code hooks (see
https://developers.openai.com/codex/hooks), so this change introduces:

- New codex/ package re-exporting the relevant claude types and helpers
  with Codex-specific documentation (Stop, PreToolUse, PermissionRequest,
  PostToolUse, UserPromptSubmit, SessionStart).
- PlatformCodex constant and codex- handler registrations on every
  unified handler (OnStop, OnBeforeExecution, OnAfterFileEdit,
  OnPromptSubmit). StopContext.ShouldSkip() now also honors
  StopHookActive for Codex.
- OnAfterFileEdit's codex-post-tool-use registration recognizes
  apply_patch in addition to Write and Edit, matching the upstream tool
  surface.
- hookshot install --codex writes ~/.codex/hooks.json with appropriate
  matchers (Bash|apply_patch for PreToolUse, apply_patch|Edit|Write for
  PostToolUse). The installer reminds users that codex_hooks = true
  under [features] in ~/.codex/config.toml is still required.
- Tests verify that the codex handlers are registered alongside the
  existing platforms and that PlatformCodex behaves like Claude/Droid
  for ShouldSkip().
- README, doc.go, docs/reference-unified.md, examples/multi-hook, and a
  new docs/reference-codex.md document the platform end-to-end.

Co-authored-by: Ashwin Ramaswami <ashwin@corridor.dev>
@ashwin-corridor ashwin-corridor marked this pull request as ready for review May 14, 2026 07:51

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread unified.go Outdated

@corridor-security corridor-security 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.

Security Issues

  • Codex ask decisions fall open: The unified Codex handler returns an ask permission decision even though Codex currently does not enforce it, allowing policies that require user confirmation to be bypassed. The codex/types.go and codex/helpers.go package documentation explicitly acknowledges this: "allow" and "ask" values are "parsed but fall open today." Any OnBeforeExecution policy that returns Ask for a risky Bash or apply_patch call will not surface the approval prompt and the tool will execute immediately.
  • Codex installer does not hook MCP tools: The generated Codex PreToolUse matcher ("Bash|apply_patch") excludes the mcp__* tool-name pattern. While unified.go's codex-pre-tool-use handler correctly classifies MCP tools (lines 464–465), Codex never invokes the hook binary for MCP calls because the matcher never matches. Any OnBeforeExecution policy meant to enforce MCP allowlists or SSRF controls is therefore silently bypassed for all Codex MCP tool calls.
  • Codex apply_patch edits are not parsed for file-edit policies: The OnAfterFileEdit Codex handler unmarshals apply_patch tool input using a struct that expects file_path and content fields, but apply_patch actually carries a command field (per reference-codex.md line 125 and the Codex upstream spec). For apply_patch calls, both toolInput.FilePath and toolInput.Content are empty after unmarshalling, so the FileEditContext passed to handlers has an empty FilePath and an empty edit payload, causing path- and secret-based policies in OnAfterFileEdit to evaluate against blank values and fail to detect policy violations.

Summary: This PR adds OpenAI Codex hook support and installer configuration, expanding security enforcement to a new AI agent platform.

Risk: High risk. The added Codex integration contains three protection-bypass paths: approval decisions fail open, MCP tool calls bypass pre-execution policies entirely, and apply_patch file-edit metadata is lost before file-edit policies can evaluate it.

Recommendations:

  • Fail closed for Codex Ask decisions until Codex enforces them (substitute claude.Deny until the platform supports ask).
  • Update the generated Codex PreToolUse matcher from "Bash|apply_patch" to "Bash|apply_patch|mcp__.*" so MCP tool calls reach the hook handler.
  • Parse apply_patch tool input separately to extract the target file path and patch content before constructing FileEditContext, or fail closed (block) for apply_patch until proper parsing is implemented.

Comment thread unified.go Outdated
Comment thread cmd/hookshot/main.go Outdated
Comment thread unified.go Outdated
… tools

Addresses three review findings on the Codex platform integration:

1. codex-post-tool-use was unmarshalling apply_patch tool_input with the
   Write/Edit schema (file_path/content/old_string/new_string), but
   apply_patch actually carries a unified-diff envelope under
   tool_input.command. The handler now parses that envelope (Add /
   Update / Delete File sections with hunked diff bodies) via the new
   parseCodexApplyPatch helper and invokes OnAfterFileEdit handlers once
   per file with a fully populated FileEditContext. If any per-file
   invocation returns FileEditBlock, the reasons are concatenated and
   emitted as a single PostToolBlock so Codex still sees feedback. The
   parser is tolerant: malformed patches fall back to a single
   invocation with the raw command text in Edits[0].NewString so events
   never disappear silently.

2. codex-pre-tool-use was returning claude.Ask(...) for AskExecution
   decisions, but Codex parses but does not enforce permissionDecision
   'ask' today. Any policy that required user confirmation before risky
   Bash or apply_patch operations was silently failing open. The
   unified bridge now rewrites Ask -> Deny on Codex so the same policy
   that asks on Claude/Cursor/Droid/Cascade fails closed on Codex.

3. The installer was generating a PreToolUse matcher of
   'Bash|apply_patch' that excluded mcp__* tool names, so the
   codex-pre-tool-use handler was never invoked for MCP calls and
   MCP allowlist/SSRF policies were structurally bypassed. The matcher
   is now 'Bash|apply_patch|mcp__.*' for PreToolUse and
   'apply_patch|Edit|Write|mcp__.*' for PostToolUse. apply_patch
   tool_input.command is also exposed via ExecutionContext.Command in
   the pre-tool-use bridge so policies can inspect the patch directly.

Adds parseCodexApplyPatch with eight unit tests covering Add, Update,
Delete, multi-hunk Update, multi-file patches, Move to renames, leading
here-doc wrappers, and malformed input. Adds five integration tests
covering the rewritten codex-pre-tool-use and codex-post-tool-use
handlers (Ask fails closed, apply_patch populates Command, MCP is
classified as ExecutionMCP, apply_patch parses files+edits, and block
propagation across multi-file patches).

Updates docs/reference-codex.md, docs/reference-unified.md, README.md,
codex/doc.go, and examples/multi-hook to reflect the new matchers, the
apply_patch parsing semantics, and the fail-closed treatment of Ask on
Codex.

Co-authored-by: Ashwin Ramaswami <ashwin@corridor.dev>

@corridor-security corridor-security 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.

Security Issues

  • Codex apply_patch move destination is not exposed to file-edit policies
    The Codex apply_patch parser correctly captures *** Move to: destinations in f.NewFilePath, but the OnAfterFileEdit bridge (unified.go:793-808) only populates FileEditContext.FilePath with the source path. Because FileEditContext has no destination-path field, any registered handler that enforces path-based policies (e.g., restricting edits to ctx.Cwd, blocking .env or SSH paths) will evaluate only the benign source path and silently approve the move. A crafted patch such as *** Update File: safe/file.txt followed by *** Move to: ../../.ssh/authorized_keys will pass every path check while writing attacker-controlled content to an unrestricted destination. The fix requires adding a NewFilePath (or DestinationPath) field to FileEditContext and populating it in the loop, then invoking the handler (or an additional handler call) for the destination path so policies can validate both endpoints of the move operation.

Comment thread unified.go Outdated
cursoragent and others added 4 commits May 14, 2026 18:42
…Edit

The codex-post-tool-use bridge was reading the *** Move to: destination
into f.NewFilePath but never surfacing it to handlers. FileEditContext
only carried FilePath (the source), so a FilePath-only path allowlist
would approve a patch like:

    *** Update File: app/config.go
    *** Move to: ../../.ssh/authorized_keys

even though the actual destination is outside the allowed directory.

Adds a NewFilePath field to FileEditContext and rewrites the codex
apply_patch loop to invoke the user's OnAfterFileEdit handler twice for
rename operations:

  1. once with FilePath = source, NewFilePath = destination
  2. once with FilePath = destination, NewFilePath = destination

Existing policies that only inspect ctx.FilePath now see the
destination as a separate event and can block it. Policies that want
to detect the rename relationship can check
ctx.NewFilePath != "" && ctx.NewFilePath != ctx.FilePath.

Block decisions across all invocations (per-file + per-rename-side)
flow through the existing block-reason aggregation, so a single
PostToolBlock with the combined reasons is emitted.

NewFilePath defaults to "" on every other platform; in-place Codex
edits also leave it empty, so this is a strictly additive change.

Tests:
- TestCodexPostToolUse_ApplyPatchMoveToInvokesHandlerForDestination
  asserts the handler is invoked once with FilePath=source and once
  with FilePath=destination, and that NewFilePath is populated on both.
- TestCodexPostToolUse_ApplyPatchMoveToBlockOnDestinationPropagates
  asserts a FilePath-only block targeting the destination results in
  a PostToolBlock with the destination's reason.

Docs:
- docs/reference-unified.md and docs/reference-codex.md describe the
  new field, the double-invocation semantics for renames, and the
  recommended check for rename detection.

Co-authored-by: Ashwin Ramaswami <ashwin@corridor.dev>

@corridor-security corridor-security 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.

Security Issues

  • Fail-open Codex PreToolUse helpers
    codex.AllowWithInput and codex.Ask are exported helpers that the accompanying comments explicitly acknowledge are not enforced by Codex. A hook author writing a platform-specific Codex handler (i.e., using the codex package directly rather than the unified hookshot.OnBeforeExecution bridge) who calls codex.AllowWithInput to sanitize a Bash/apply_patch payload will have the updatedInput silently ignored — Codex executes the original, unsanitized input. Likewise, a hook using codex.Ask to gate execution on user confirmation will fall open: Codex parses permissionDecision: "ask" but does not surface a prompt, so the tool runs without confirmation. The unified bridge (unified.go) does convert AskExecution() to Deny for Codex, but that protection only covers hooks written against the unified API — direct consumers of the codex package helpers remain exposed.

Recommendation

  • Remove or replace the fail-open helpers in codex/helpers.go. For Ask, replace the re-export with a wrapper that returns claude.Deny(reason), matching the unified bridge's behaviour. For AllowWithInput, either remove the export entirely until Codex enforces updatedInput, or replace it with a wrapper that returns claude.Deny(reason) so callers fail closed rather than silently losing the input rewrite.

Comment thread codex/helpers.go Outdated
…fied via codex package

Codex's PreToolUse rejects hook output containing suppressOutput,
updatedInput, continue:false, stopReason — the CLI emits
"PreToolUse hook returned unsupported <field>" rather than ignoring the
field (despite the upstream docs claiming fail-open). Two changes:

1. Make codex.AllowSilent and codex.AllowWithInput actually diverge from
   the claude.* aliases instead of inheriting the broken JSON shape.
   AllowSilent now emits {} (no suppressOutput); AllowWithInput drops
   the updatedInput map and preserves the reason via permissionDecision:
   "allow".

2. Switch every unified Codex registration (codex-stop,
   codex-pre-tool-use, codex-post-tool-use, codex-user-prompt-submit) to
   call codex.* helpers instead of claude.* so future Codex quirks live
   in the codex package, not the unified bridge.

Updates docs/reference-codex.md to correctly mark the four fields as
fail-closed. Adds regression tests in codex/helpers_test.go and
unified_test.go.

Co-authored-by: Cursor <cursoragent@cursor.com>

@corridor-security corridor-security 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.

Security Issues

  • Codex MCP deny can fail open
    The new Codex unified PreToolUse bridge in unified.go classifies mcp__* tools as ExecutionMCP and routes all deny/ask decisions through codex.Deny(...). However, the codex/types.go doc comment, codex/helpers.go, and docs/reference-codex.md all explicitly confirm that Codex only enforces permissionDecision: "deny" for Bash and apply_patch tools — MCP tools are not mentioned. A hook that returns DenyExecution() for an MCP tool on Codex will have its deny output parsed but silently ignored, and the MCP tool will execute anyway. This allows an agent running under Codex to bypass any MCP-level policy — including SSRF allowlists or network-access controls — that a hookshot hook was intended to enforce.

Recommendation
In the codex-pre-tool-use handler (unified.go:508–516), add a guard that detects ExecutionMCP + a non-allow decision and falls back to a Codex-enforced blocking mechanism. Options include: returning an error/exit-code-2 via RunE if Codex honors that for MCP tools; using the PermissionRequest event handler (which does have enforced deny support) as the primary gate for MCP approval on Codex; or explicitly refusing to register the codex-pre-tool-use handler for MCP tools until Codex enforces the deny. Document the current limitation clearly in both codex/helpers.go and docs/reference-codex.md, and add an integration test that verifies a denied mcp__* call is actually blocked on Codex.

Comment thread unified.go

@corridor-security corridor-security 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.

Security Issues

  • Codex PostToolUse Hook Bypass (Confirmed)
    The Codex installer config in cmd/hookshot/main.go sets the PostToolUse matcher to "apply_patch|mcp__.*", omitting Bash. Codex routes file edits through Bash apply_patch heredocs at least as often as native apply_patch tool calls — evidenced by production session observations and an explicit code comment noting that "the SecurityScanResults dash stayed empty for every Codex session" prior to the Bash bridging logic being added. The codex-post-tool-use handler (unified.go:874–889) contains a fully-implemented case "Bash" branch that calls codex.ParseApplyPatchFromBash and dispatches per-file through OnAfterFileEdit, but this branch is dead code because the missing Bash matcher prevents the hook from ever firing for Bash tool calls. All file/path/content security policies registered via OnAfterFileEdit are silently bypassed for every Codex Bash-wrapped patch, including path blocklists, content scanning, and audit telemetry.

Recommendations

  • Add Bash to the Codex PostToolUse matcher: change "apply_patch|mcp__.*" to "Bash|apply_patch|mcp__.*" at cmd/hookshot/main.go:505. The handler already safely short-circuits non-apply_patch Bash commands via codex.ParseApplyPatchFromBash returning false.

Comment thread cmd/hookshot/main.go
@ashwin-corridor ashwin-corridor force-pushed the cursor/add-codex-support-b167 branch from b7889bc to 496112b Compare May 18, 2026 03:10

@corridor-security corridor-security 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.

Security Issues

  • Codex PostToolUse hook bypass via missing Bash matcher (cmd/hookshot/main.go:505): installCodex configures codex-post-tool-use with "matcher": "apply_patch|mcp__.*" for PostToolUse. The hook binary's case "Bash" handler in unified.go:874 — which calls ParseApplyPatchFromBash and dispatches OnAfterFileEdit for heredoc-style edits — is never reached because Codex never fires the PostToolUse hook for Bash tool events. All file edits Codex routes through Bash heredocs (apply_patch <<'PATCH' ...) therefore bypass every OnAfterFileEdit security policy silently. Fix: add Bash to the PostToolUse matcher.
  • Codex Bash apply_patch detector bypass via prepended marker (codex/apply_patch.go:197): ParseApplyPatchFromBash uses strings.Index to find the first *** Begin Patch marker, then checks only the text before that marker for an apply_patch << invocation. A Codex-controlled Bash command can prepend a decoy marker (e.g. printf '*** Begin Patch\n' >/dev/null) so that strings.Index anchors to the decoy, the real apply_patch invocation falls after it, the prefix check fails, and the function returns ok=false — causing the actual patch to bypass all OnAfterFileEdit policies. Fix: search for the apply_patch <<HEREDOC invocation first, then locate the *** Begin Patch envelope that follows it.

Comment thread codex/apply_patch.go Outdated
OnPromptSubmit was constructing the PromptContext without Cwd for the
claude-user-prompt-submit and droid-user-prompt-submit handlers, even
though input.Cwd is always populated by Claude Code and Droid when
running in a project directory.

Without this, ctx.Cwd was empty in every OnPromptSubmit handler for
these two platforms, forcing callers to fall back to os.Getwd() or
env-var heuristics (CLAUDE_PROJECT_DIR). The Codex handler already
forwarded Cwd correctly.

Co-authored-by: Cursor <cursoragent@cursor.com>

@corridor-security corridor-security 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.

Security Issues

  • Codex PostToolUse hook bypass (cmd/hookshot/main.go:505)
    The default Codex installer configures PostToolUse with matcher "apply_patch|mcp__.*", omitting Bash. The codex-post-tool-use handler explicitly handles ToolName == "Bash" to detect heredoc-wrapped apply_patch edits — but this branch is never reached because the hook is never triggered for Bash tool calls. Codex routes file edits through Bash heredocs at least as often as through first-class apply_patch calls. Any such edit silently bypasses OnAfterFileEdit policies (secret scanning, path allowlists, etc.).

    Fix: Change line 505 in cmd/hookshot/main.go: "matcher": "Bash|apply_patch|mcp__.*"

  • Incomplete Bash apply_patch detection (codex/apply_patch.go:14)
    The regex (?:^|[/\s;&|])apply_patch[ \t]+<<-? requires mandatory whitespace between apply_patch and <<, and accepts no whitespace after the operator. Shell-valid forms apply_patch<<'PATCH' (no leading space) and apply_patch << 'PATCH' (space after <<) both execute but return ok=false from ParseApplyPatchFromBash, silently skipping OnAfterFileEdit checks for those edits.

    Fix: Broaden the regex to accept optional whitespace on both sides of the heredoc operator: (?:^|[/\s;&|])apply_patch(?:[ \t]+|(?=<<))<<-?[ \t]*

Comment thread codex/apply_patch.go Outdated
epicfaace and others added 2 commits May 17, 2026 20:57
Codex 0.130.0+ routes greenfield file writes through plain Bash
`cat <<'EOF' > FILE … EOF` (and `tee FILE <<'EOF' … EOF`) rather than
the apply_patch tool or any Write/Edit alias. ParseApplyPatchFromBash
correctly rejects these shapes, so the unified codex-post-tool-use
bridge silently no-op'd every "create a file" Bash invocation — no
afterFileEdit fired, no security scan ran, no PostToolUse telemetry
was emitted. End-to-end probe captured Codex emitting:

  tool_name: "Bash"
  tool_input.command: "cat <<'EOF' > greet.txt\nhello world\nEOF"

Add codex.ParseBashRedirectWrite, paralleling ParseApplyPatchFromBash:

  - Detects cat / tee heredocs with `>`/`>>` redirects.
  - Handles quoted (`<<'EOF'`, `<<"EOF"`) and unquoted (`<<EOF`)
    delimiters plus `<<-DELIM` tab-stripping.
  - Path is preserved verbatim so downstream policies can reject
    suspicious paths (e.g. `../../etc/passwd`) themselves.
  - Bails out (ok=false) on missing terminator rather than guessing.

Wire it into codex-post-tool-use as a fallback after the apply_patch
check, so edits keep their higher-fidelity per-hunk shape while
greenfield writes now flow through the same dispatchPatch reducer.

Tests: - codex/bash_write_test.go: 13 unit tests covering every variant.
  - unified_test.go: integration tests for dispatch + block propagation.
Co-authored-by: Cursor <cursoragent@cursor.com>
The hookshot installer's PostToolUse matcher was `apply_patch|mcp__.*`,
which never matches anything Codex 0.130.0+ actually emits for file
edits — Codex routes both edits (`apply_patch <<'PATCH' …`) and writes
(`cat <<'EOF' > FILE …`) through `tool_name="Bash"`, not the
first-class apply_patch tool. Without Bash in the matcher, Codex never
hands the events to the hook binary, so the freshly-added
ParseBashRedirectWrite dispatch never gets a chance to fire and
afterFileEdit telemetry stays empty for every Codex session.

Update the installer to write `Bash|apply_patch|mcp__.*` for both
PreToolUse and PostToolUse, matching what hookshot's unified bridge
parses. Sync README, codex/doc.go, docs/reference-codex.md,
docs/reference-unified.md, and examples/multi-hook to the new
recommended matcher so copy-pasted configs stay correct.

Co-authored-by: Cursor <cursoragent@cursor.com>

@corridor-security corridor-security 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.

Security Issues

  • Security Hook Bypass (Validated)
    The new Codex Bash heredoc write parser (codex/bash_write.go) only processes the first matching cat/tee heredoc redirect in a Bash command. matchWriteRedirect calls re.FindStringSubmatchIndex (line 107), which returns only the first regex match; ParseBashRedirectWrite returns immediately after the first successful match at line 96. A Codex Bash command containing a benign first heredoc write followed by one or more sensitive writes causes OnAfterFileEdit to dispatch a FileEditContext only for the first file. All subsequent writes — to paths such as .env, ~/.ssh/authorized_keys, or any content-scanned target — are silently skipped by the post-tool bridge, bypassing all path-deny rules and secret/content scanners registered via OnAfterFileEdit.

Recommendations

  • In matchWriteRedirect, replace re.FindStringSubmatchIndex with re.FindAllStringSubmatchIndex and iterate over every match, building a PatchFile for each well-formed heredoc write. Accumulate and return all results rather than returning after the first.
  • If a match is found but the heredoc body cannot be parsed (missing terminator, ambiguous delimiter), fail closed: return (nil, false) so the caller in unified.go falls through to the raw-command fallback path, which surfaces the full command to policy handlers rather than silently skipping it.
  • Add a test case covering a command that contains two cat <<EOF > writes with distinct targets and verify both PatchFile entries are returned.

Comment thread codex/bash_write.go
Comment thread docs/reference-codex.md
Adds a dedicated 'Bash bridge: file edits routed through Bash' section
to docs/reference-codex.md that explains why Codex 0.130.0+ sessions
route file edits through plain Bash invocations rather than the native
apply_patch / Write / Edit tools, and how OnAfterFileEdit reduces all
four shapes to one per-file FileEditContext.

The new section covers:

- The four shapes the bridge recognizes today (Write/Edit native,
  apply_patch native, Bash apply_patch <<'PATCH' heredoc, Bash
  cat/tee <<'EOF' > FILE heredoc) with the parser used for each.
- Why the PostToolUse matcher must include 'Bash' — the symptom of
  not including it is zero afterFileEdit events for any Codex
  session that edits files.
- Detection rules for apply_patch via Bash heredoc, including the
  per-session shim-path variant.
- How greenfield writes synthesize a single PatchEdit so they flow
  through the same dispatch path as diff-based edits, and which
  cat/tee variants (quoted/unquoted/<<- delimiters, >/>>, cd-prefix)
  are recognized.
- Public API for handlers that want to bypass the unified bridge:
  ParseApplyPatch, ParseApplyPatchFromBash, ParseBashRedirectWrite,
  PatchFile, PatchEdit — with an example handler.
- Known limitations: echo/printf > FILE not recognized, only the
  first cat/tee heredoc in a single Bash command is surfaced, and
  paths are passed through verbatim without normalization. Each
  limitation comes with a concrete defence-in-depth suggestion
  (OnBeforeExecution grep, filepath.Clean) so policy authors can
  close the gap themselves until the parsers grow stricter.

The existing PostToolUse events-table row now points at both the new
'Bash bridge' section and the existing 'apply_patch on the unified
API' section so readers land on the right page regardless of which
shape they're investigating.

Co-authored-by: Ashwin Ramaswami <ashwin@corridor.dev>

@jake-corridor jake-corridor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

mostly nits; but please see my comment on installCodex, that is probably worth addressing

Comment thread cmd/hookshot/main.go Outdated
Comment thread cmd/hookshot/main.go
// "mcp__.*" so MCP tool calls reach the hook binary. "apply_patch"
// alone covers Codex file edits — Codex emits "Edit" and "Write" as
// matcher aliases for apply_patch, so they're redundant here.
hooks := map[string]any{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Elsewhere, we are using strongly-typed structs when working JSON that has a know schema: claude/types.go, codex/types.go, etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment thread codex/apply_patch.go Outdated
Comment thread codex/apply_patch.go Outdated
Comment thread codex/bash_write.go
Comment thread codex/bash_write.go Outdated
Comment thread unified.go

@corridor-security corridor-security 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.

Security Issues

  • Path Traversal Policy Bypass via Quoted Paths
    The Codex Bash heredoc parser captures redirection paths using the regex (?P<path>[^\s;&|]+), which treats shell quote characters as ordinary path characters. A quoted traversal target like '../../.ssh/authorized_keys' is captured verbatim — including the surrounding single-quotes — and stored in FileEditContext.FilePath. Downstream hook policies applying filepath.Clean/filepath.Rel containment checks operate on the quoted string (e.g. '../../.ssh/authorized_keys'), which starts with a quote character, not .., causing those checks to silently pass while Bash still writes to the actual unquoted path. The same raw path capture group affects both the catRedirectHeredocRE and teeRedirectHeredocRE patterns.

  • File Edit Hook Bypass — Multiple Heredoc Writes
    The matchWriteRedirect function uses regexp.FindStringSubmatchIndex, which returns only the first match in a command string. A Bash command can chain multiple heredoc writes with ; or && separators; only the first file is dispatched to OnAfterFileEdit, leaving every subsequent write invisible to path/content security policies. The documentation acknowledges this as a known gap and recommends registering an OnBeforeExecution policy as defense-in-depth, but implementations that rely solely on OnAfterFileEdit remain vulnerable.

Comment thread codex/bash_write.go Outdated

@corridor-security corridor-security 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.

Security Issues

  • Path policy bypass via shell-expanded redirection targets
    wordText returns the raw shell source text (e.g., "$HOME/.ssh/authorized_keys") for redirection targets that contain variable expansions, command substitutions, or globs. While this is documented as intentional so that downstream policy handlers can still inspect the surface form, it creates a systematic footgun: handlers performing a filepath.Clean/filepath.Rel containment check against ctx.Cwd will treat the unexpanded string as a literal relative path inside the project root, causing the check to pass silently even though Bash will write the file to an expanded, potentially out-of-tree location. Any hook author who writes a standard cwd-containment policy without specially handling non-literal paths is vulnerable. The fix is for wordText (or its callers) to signal when a path contains shell expansions, allowing the bridge to fail closed rather than forwarding a misleading path.

  • Path policy bypass via multi-target tee writes
    teeRedirectWrite unconditionally breaks after recording the first non-option argument, but tee writes to every positional file operand. A command such as tee allowed.txt ../../.ssh/authorized_keys <<'EOF'\n...\nEOF produces two real files, yet the OnAfterFileEdit bridge only fires for allowed.txt. Path-based deny rules for every subsequent operand are silently skipped. The fix is to iterate all non-option arguments and return one PatchFile per target, failing closed if any target contains an unresolvable shell expansion.

Comment thread codex/bash_write.go Outdated
continue
}
path = wordText(command, arg)
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

teeRedirectWrite stops after the first non-option argument:

for _, arg := range call.Args[1:] {
	lit := arg.Lit()
	if strings.HasPrefix(lit, "-") {
		continue
	}
	path = wordText(command, arg)
	break
}

However, tee writes to every file operand. A command such as tee allowed.txt ../../.ssh/authorized_keys <<'EOF'\n...\nEOF creates both files, but the OnAfterFileEdit bridge only invokes the policy handler for allowed.txt. Any path-based deny rule for the second (and every subsequent) target is silently skipped.

Unlike the shell-expansion footgun, this bypass requires no special trick: all paths are plain literals that would pass wordToLiteral, so even a handler with a correct cwd-containment policy is bypassed because the dangerous path is never delivered to it.

Remediation: Remove the break and collect all non-option positional arguments into a []string. Adjust bashRedirectWriteFromStmt / ParseBashRedirectWrite to return one PatchFile per target path. Fail closed if any target is an unparseable shell expansion. Add a regression test covering tee FILE1 FILE2 <<'EOF'.

Attack Path
  1. Codex emits: tee allowed.txt ../../.ssh/authorized_keys <<'EOF'\nattacker-key\nEOF

  2. ParseBashRedirectWrite parses successfully; teeRedirectWrite is called via bashRedirectWriteFromStmt.

  3. teeRedirectWrite records allowed.txt as path and breaks — ../../.ssh/authorized_keys is never visited.

  4. A single PatchFile{FilePath: "allowed.txt"} is returned; dispatchPatch calls the handler once for allowed.txt, which passes the containment check.

  5. Codex receives PostToolOK(); Bash writes both allowed.txt and ../../.ssh/authorized_keys. The sensitive write bypassed every registered policy.

For more details, see the finding in Corridor.

Provide feedback: Reply with whether this is a valid vulnerability or false positive to help improve Corridor's accuracy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed in 059943e

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the quick fix. I’ve marked this finding as a true positive and noted it as resolved in 059943e.

@corridor-security corridor-security 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.

Security Issues

  • Codex cat heredoc write target confusion: catRedirectWrite records the last redirect matching syntax.RdrOut/syntax.AppOut without inspecting the fd-number field r.N. In mvdan.cc/sh/v3, stderr redirects (2>) use the same RdrOut operator as stdout redirects (>), distinguished only by r.N being set to "2". So a Bash command like cat <<EOF > .env 2> allowed.log causes the parser to overwrite out with the 2> allowed.log redirect (because it appears later in stmt.Redirs) and reports allowed.log to OnAfterFileEdit instead of .env. Any path or secret policy that would block writes to .env never fires.

  • Codex tee multi-target bypass: teeRedirectWrite breaks out of the argument loop after the first non-option filename. tee writes the heredoc body to every positional filename argument. A command like tee allowed.txt .env <<'EOF' writes to both files but only allowed.txt is dispatched to OnAfterFileEdit, leaving the .env write invisible to all security policies.

Recommendations

  • For cat: Only treat a redirect as the stdout write target when r.N is nil, empty, or "1". Redirects with r.N.Value == "2" (or any other non-stdout fd) should be ignored when selecting the heredoc body destination.
  • For tee: Collect all non-option target filenames and dispatch a PatchFile for each one, or fail closed when multiple targets are present.

Comment thread codex/bash_write.go

@corridor-security corridor-security 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.

Security Issues

  • Codex Bash file-write monitoring bypass
    The Codex bridge (unified.go:910-913) falls through to PostToolOK() for any Bash command that both ParseApplyPatchFromBash and ParseBashRedirectWrite fail to match. ParseBashRedirectWrite itself only detects heredoc writes (<<) whose primary command is the literal cat or tee (bash_write.go:115-121). Any other file-write form — echo ... > .env, printf ... > .env, /bin/cat <<EOF > .env, command cat <<EOF > .env, or any write lacking a << — returns (nil, false) from both parsers and therefore bypasses OnAfterFileEdit entirely, silently skipping path-deny rules and secret scanners. The TestParseBashRedirectWrite_NoHeredoc_PlainBash test explicitly documents that plain-redirect forms such as echo hello > greet.txt and printf 'hi' > greet.txt are out of scope and will trigger this bypass.

  • Path containment bypass for shell-expanded redirect targets
    When a heredoc redirect target contains shell metacharacters (parameter expansion such as $HOME, command substitution, arithmetic expansion, or tilde), wordToLiteral (bash_write.go:294-315) returns ("", false). wordText then falls back to the verbatim source slice (bash_write.go:282-287), returning a string such as "$HOME/outside.txt" (with the surrounding quotes included). This non-empty string is stored in PatchFile.FilePath and surfaced as ctx.FilePath in FileEditContext. A policy that evaluates filepath.Rel(ctx.Cwd, ctx.FilePath) will see a string beginning with " rather than .., so a strings.HasPrefix(rel, "..") containment check incorrectly passes while Bash actually expands $HOME at runtime and writes outside the workspace.

Recommendations

  • For the write-monitoring bypass: either extend ParseBashRedirectWrite to detect all Bash write-redirect forms (including non-heredoc > / >> redirections and absolute-path or wrapped command variants), or fail closed by detecting any stdout-targeting redirect in the AST and dispatching a synthetic FileEditContext with an empty FilePath so policies are forced to make an explicit decision rather than silently allowing the write.
  • For the path containment bypass: reject or explicitly mark redirect targets that wordToLiteral cannot fully resolve as unsafe/unknown — return (nil, false) from catRedirectWrite / teeRedirectWrite when wordText returns a raw expansion-containing string — so policies never receive a ctx.FilePath that cannot be reliably compared against ctx.Cwd.

Comment thread codex/bash_write.go
return nil, false
}
switch call.Args[0].Lit() {
case "cat":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This parser only recognizes heredoc writes when the primary command is the literal cat or tee:

switch call.Args[0].Lit() {
case "cat":
	return catRedirectWrite(command, stmt)
case "tee":
	return teeRedirectWrite(command, stmt, call)
}
return nil, false

In the Codex bridge (unified.go:910-913), when both ParseApplyPatchFromBash and ParseBashRedirectWrite return ok=false, the handler falls through to codex.PostToolOK() without invoking OnAfterFileEdit. An agent can therefore write files using other valid Bash forms:

  • printf '%s\n' '[REDACTED_SECRET]' > .env (no heredoc, non-cat/tee command)
  • echo ... > .env (no heredoc)
  • /bin/cat <<EOF > .env (absolute path: call.Args[0].Lit() returns "")
  • command cat <<EOF > .env (builtin wrapper: primary token is command)

The TestParseBashRedirectWrite_NoHeredoc_PlainBash test (bash_write_test.go:126-141) explicitly documents that echo hello > greet.txt and printf 'hi' > greet.txt are out-of-scope and return ok=false. This bypasses all path-deny rules and secret scanners implemented on the unified file-edit hook.

Remediation: Add a fallback redirect detector after both parsers fail. Walk the Bash AST for any RdrOut/AppOut/RdrAll/AppAll redirect on stdout; if found, dispatch a synthetic FileEditContext with FilePath: "" (or a <unknown-bash-write> sentinel) so policies are forced to make an explicit allow/block decision rather than silently passing the write.

Attack Path
  1. Codex agent issues a Bash tool call: printf 'STRIPE_KEY=[REDACTED_API_KEY]\n' > .env

  2. codex-post-tool-use hook fires; bridge at unified.go:907 calls ParseApplyPatchFromBash(nil, false) (no patch marker).

  3. Bridge at unified.go:910 calls ParseBashRedirectWrite(nil, false) (no << in command, pre-check at bash_write.go:78 short-circuits).

  4. Bridge falls through to return codex.PostToolOK() at unified.go:913OnAfterFileEdit is never invoked.

  5. Path-deny rules not evaluated; secret scanners do not run; no SecurityScanResult telemetry emitted. File write proceeds on disk.

For more details, see the finding in Corridor.

Provide feedback: Reply with whether this is a valid vulnerability or false positive to help improve Corridor's accuracy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

that's ok

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the quick reply. To make sure I handle this correctly, can you confirm whether you want to: (a) accept the risk and override the gate for this PR, (b) mark this as a false positive/out of scope, or (c) treat it as a valid issue and keep the gate until a fix? Today, commands like echo/printf > file bypass OnAfterFileEdit in the bridge (unified.go:910–913), which skips path-deny rules and secret scanning (CWE-693). If you’d like a lightweight mitigation, I can propose a patch to add a fallback redirect detector (scan AST for >, >>, &>, &>>) and dispatch a synthetic FileEditContext so policies are forced to decide (fail closed per OWASP best practices). Let me know which option you prefer and I’ll proceed accordingly.

Comment thread codex/bash_write.go
// fallback matters for paths like `$HOME/.env` where downstream policy
// still needs to see the surface text even though Bash would expand it.
func wordText(command string, w *syntax.Word) string {
if w == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

wordText falls back to returning the raw shell source for redirect targets that require expansion:

start := int(w.Pos().Offset())
end := int(w.End().Offset())
if start < 0 || end > len(command) || start >= end {
    return ""
}
return command[start:end]

For a target like "$HOME/outside.txt", wordToLiteral returns ("", false) because the DblQuoted word contains a ParamExp node. wordText then returns command[start:end] = "$HOME/outside.txt" including the outer double-quote characters (the AST offsets span the full quoted token). This non-empty string passes the path == "" guard at bash_write.go:164 and is stored in PatchFile.FilePath, then surfaced as ctx.FilePath in FileEditContext (unified.go:804).

A policy performing filepath.Rel(ctx.Cwd, ctx.FilePath) receives a path that starts with " (a double-quote character) rather than / or ... A strings.HasPrefix(rel, "..") containment check silently passes while Bash actually expands $HOME at runtime and writes to an absolute path outside the workspace.

This is structurally identical to the prior regex-based bypass documented in bash_write_test.go:311-333, where single-quoted paths caused the same '-prefix confusion — fixed for literal-quoted paths by wordToLiteral, but reintroduced by the expansion fallback in wordText.

Remediation: In catRedirectWrite and teeRedirectWrite, fail closed when wordToLiteral returns ok=false: return (nil, false) instead of calling wordText. This causes ParseBashRedirectWrite to return (nil, false) for expansion-containing targets, causing the bridge to invoke the dispatchPatch fallback with len(files)==0 (raw command as edit content, empty FilePath) — giving policies an explicit opportunity to block.

Attack Path
  1. Codex agent issues: cat <<'EOF' > "$HOME/.ssh/authorized_keys"\nattacker-key\nEOF

  2. ParseBashRedirectWrite parses the AST; catRedirectWrite calls wordText for the redirect target.

  3. wordToLiteral fails (DblQuoted contains ParamExp). wordText falls back to command[start:end] = "$HOME/.ssh/authorized_keys" (with surrounding quotes).

  4. Non-empty path passes the path == "" guard; PatchFile.FilePath = "$HOME/.ssh/authorized_keys".

  5. ctx.FilePath = "$HOME/.ssh/authorized_keys". filepath.Rel(ctx.Cwd, ctx.FilePath) returns a string starting with ", not ... Containment check passes.

  6. Handler allows the write. Bash expands $HOME at runtime and writes to the real absolute path outside the workspace.

For more details, see the finding in Corridor.

Provide feedback: Reply with whether this is a valid vulnerability or false positive to help improve Corridor's accuracy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

that's ok

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the reply. To make sure we handle this correctly, can you confirm whether you want this finding marked as a true positive or a false positive? As a reminder, the current behavior lets expansion-containing targets like "$HOME/foo" bypass path containment checks that rely on filepath.Rel + HasPrefix(".."), which is a CWE-22 style risk. If you’d like to proceed with a fix, the safest change is to fail closed in catRedirectWrite/teeRedirectWrite: when wordToLiteral returns ok=false, return (nil, false) so the bridge falls back to dispatching an edit with an empty FilePath and policy can explicitly decide. I can mark this as true positive and note the remediation, or mark it as a false positive based on your decision.

@ashwin-corridor ashwin-corridor merged commit 73584ae into main May 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants