Skip to content

fix(#6920): withhold unsafe codex tool results - #6952

Open
waynesun09 wants to merge 1 commit into
mainfrom
codex-security-hooks
Open

fix(#6920): withhold unsafe codex tool results#6952
waynesun09 wants to merge 1 commit into
mainfrom
codex-security-hooks

Conversation

@waynesun09

Copy link
Copy Markdown
Member

Summary

  • verify the shared SSRF pre-tool hook blocks loopback requests through the Codex adapter
  • withhold Codex tool results when the shared post-tool chain reports secrets or unsafe Unicode
  • allow context-only suppression and terminal-escape cleanup to pass unchanged
  • fail closed on malformed Unicode category metadata and document the runtime behavior

Test plan

  • make lint
  • go test ./internal/security -count=1
  • go test ./internal/runtime -run '^TestCodexAdapter' -count=1
  • make go-test (baseline/environment failures remain in harnessdispatch, dummy runtime, and sandbox tests; changed-path suites pass)

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Withhold unsafe Codex tool results

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Withhold Codex results when sanitizers detect secrets or unsafe Unicode.
• Pass benign context suppression and terminal-escape rewrites without warnings.
• Verify real SSRF and sanitizer chains through Codex adapter tests.
Diagram

graph TD
  A["Codex Tool"] --> B["Hook Adapter"] --> C["Post-tool Chain"] --> D{"Rewrite class"}
  D -->|Secret or unsafe Unicode| E["Withhold Result"]
  D -->|Context or escapes| F["Pass Original"]
  D -->|Unclassified| G["Warn Model"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Block every sanitizer rewrite
  • ➕ Provides simple fail-closed behavior
  • ➕ Avoids relying on sanitizer metadata classification
  • ➖ Withholds safe output for context compaction and terminal formatting
  • ➖ Could unnecessarily disrupt normal Codex workflows
2. Add an explicit rewrite disposition
  • ➕ Creates a clear contract between sanitizers and runtime adapters
  • ➕ Avoids inferring security impact from individual metadata fields
  • ➖ Requires coordinated changes across the shared hook protocol and runtimes
  • ➖ Expands the scope and regression surface of this targeted fix

Recommendation: Use the PR's metadata-based classification for this targeted security fix: it closes the exposure without penalizing benign rewrites and fails closed for malformed Unicode metadata. An explicit security-disposition field would be a cleaner long-term protocol if more sanitizer categories are introduced.

Files changed (4) +247 / -34

Bug fix (1) +58 / -5
fullsend-codex-hook.pyClassify and withhold security-sensitive sanitizer rewrites +58/-5

Classify and withhold security-sensitive sanitizer rewrites

• Adds metadata-based classification for secret and Unicode rewrites, blocking results that Codex cannot safely sanitize. Context suppression and terminal-escape-only cleanup pass silently, malformed Unicode metadata fails closed, and unknown rewrite types continue to warn the model.

internal/runtime/codex_hook/fullsend-codex-hook.py

Tests (1) +175 / -20
codex_hook_test.goCover Codex SSRF and post-tool withholding paths +175/-20

Cover Codex SSRF and post-tool withholding paths

• Adds an embedded-hook test helper and integration coverage using the real SSRF and post-tool chains. Tests verify withholding for secrets, hidden Unicode, mixed normalization findings, and malformed metadata while preserving benign cleanup and context suppression behavior.

internal/runtime/codex_hook_test.go

Documentation (2) +14 / -9
runtime-implementation.mdDocument Codex sanitizer classification and withholding behavior +8/-4

Document Codex sanitizer classification and withholding behavior

• Updates the runtime implementation guide and capability matrix to explain that Codex withholds secret and unsafe-Unicode results. It also documents unchanged handling for context suppression and terminal escapes, plus warnings for unclassified rewrites.

docs/contributing/runtime-implementation.md

codex.mdClarify user-visible Codex security hook behavior +6/-5

Clarify user-visible Codex security hook behavior

• Explains that Codex cannot edit built-in tool output, so unsafe results are withheld while benign cleanup rewrites are skipped. Artifact redaction behavior remains documented separately.

docs/runtimes/codex.md

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 10:10 AM UTC · Ended 10:17 AM UTC

Commit: 32528d3 · View workflow run →

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. context_suppressed can leak secrets 📘 Rule violation ⛨ Security
Description
Context suppression runs before secret redaction, so successful test output containing a secret can
be summarized before the redactor scans it. The adapter then treats the rewrite as benign and allows
Codex to receive the original unredacted output, contradicting the documented runtime security
behavior.
Code

internal/runtime/codex_hook/fullsend-codex-hook.py[R478-479]

+            if benign_rewrite(verdict["output"]):
+                continue
Relevance

●●● Strong

Allowing context suppression before secret redaction can expose original secrets, violating the
documented Codex security contract.

PR-#6926

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 2889480 requires runtime backend hook behavior to follow the documented security contract. The
chain at posttool_chain.py[376-433] suppresses updated before redaction scans it; the newly
changed adapter at fullsend-codex-hook.py[463-480] then permits any context_suppressed rewrite,
while the runtime guide claims secret-bearing results are withheld.

Rule 2889480: Consult runtime implementation guide when modifying runtime.Runtime backends
internal/runtime/codex_hook/fullsend-codex-hook.py[463-480]
internal/security/hooks/posttool_chain.py[376-433]
docs/contributing/runtime-implementation.md[107-109]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Codex can receive an original tool result containing secrets when context suppression removes the secret before the redaction stage scans the rewritten value.

## Issue Context
The post-tool chain performs context suppression before secret redaction. Because Codex cannot apply the summary rewrite, classifying `context_suppressed` as benign is safe only after the original result has independently passed security scanning.

## Fix Focus Areas
- internal/runtime/codex_hook/fullsend-codex-hook.py[463-480]
- internal/security/hooks/posttool_chain.py[376-433]
- internal/runtime/codex_hook_test.go[368-386]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Benign Unicode rewrites blocked 🐞 Bug ≡ Correctness
Description
security_rewrite treats every category except ANSI/OSC as removed security content, but
fullwidth is detection-only and deliberately remains unchanged. A context-suppression or ordinary
terminal-cleanup rewrite containing a fullwidth character is therefore incorrectly blocked and
withheld from Codex.
Code

internal/runtime/codex_hook/fullsend-codex-hook.py[393]

+    return any(category not in {"ansi_escape", "osc_escape"} for category in categories)
Relevance

●●● Strong

Detection-only fullwidth findings should not be blocked; this conflicts with the adapter’s benign
Unicode classification intent.

PR-#6926

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The Unicode scanner explicitly retains compatibility/fullwidth characters, except when NFKC
reconstructs an escape. The chain nevertheless copies every finding name—including detection-only
fullwidth—into metadata.categories; the new adapter then classifies any category outside
ANSI/OSC as security-sensitive, so unrelated benign rewrites are blocked.

internal/runtime/codex_hook/fullsend-codex-hook.py[377-412]
internal/security/hooks/unicode_posttool.py[169-211]
internal/security/hooks/posttool_chain.py[314-353]
internal/security/hooks/posttool_chain.py[376-391]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Codex adapter classifies all Unicode metadata categories as rewrite causes, although `fullwidth` is only detected and retained. Consequently, a benign context-suppression or direct terminal-escape rewrite can be withheld merely because its input also contained a compatibility character.

## Issue Context
The distinction must preserve blocking for escapes reconstructed through NFKC while allowing independent ANSI/OSC cleanup and context suppression. The existing category list cannot distinguish a directly removed escape from an NFKC-reassembled escape.

## Fix Focus Areas
- internal/runtime/codex_hook/fullsend-codex-hook.py[377-412]
- internal/security/hooks/posttool_chain.py[314-353]
- internal/security/hooks/unicode_posttool.py[169-211]
- internal/runtime/codex_hook_test.go[285-386]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 65 rules
Review mode: ⚖️ Balanced

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +478 to +479
if benign_rewrite(verdict["output"]):
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. context_suppressed can leak secrets 📘 Rule violation ⛨ Security

Context suppression runs before secret redaction, so successful test output containing a secret can
be summarized before the redactor scans it. The adapter then treats the rewrite as benign and allows
Codex to receive the original unredacted output, contradicting the documented runtime security
behavior.
Agent Prompt
## Issue description
Codex can receive an original tool result containing secrets when context suppression removes the secret before the redaction stage scans the rewritten value.

## Issue Context
The post-tool chain performs context suppression before secret redaction. Because Codex cannot apply the summary rewrite, classifying `context_suppressed` as benign is safe only after the original result has independently passed security scanning.

## Fix Focus Areas
- internal/runtime/codex_hook/fullsend-codex-hook.py[463-480]
- internal/security/hooks/posttool_chain.py[376-433]
- internal/runtime/codex_hook_test.go[368-386]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

return True
if not all(isinstance(category, str) for category in categories):
return True
return any(category not in {"ansi_escape", "osc_escape"} for category in categories)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Benign unicode rewrites blocked 🐞 Bug ≡ Correctness

security_rewrite treats every category except ANSI/OSC as removed security content, but
fullwidth is detection-only and deliberately remains unchanged. A context-suppression or ordinary
terminal-cleanup rewrite containing a fullwidth character is therefore incorrectly blocked and
withheld from Codex.
Agent Prompt
## Issue description
The Codex adapter classifies all Unicode metadata categories as rewrite causes, although `fullwidth` is only detected and retained. Consequently, a benign context-suppression or direct terminal-escape rewrite can be withheld merely because its input also contained a compatibility character.

## Issue Context
The distinction must preserve blocking for escapes reconstructed through NFKC while allowing independent ANSI/OSC cleanup and context suppression. The existing category list cannot distinguish a directly removed escape from an NFKC-reassembled escape.

## Fix Focus Areas
- internal/runtime/codex_hook/fullsend-codex-hook.py[377-412]
- internal/security/hooks/posttool_chain.py[314-353]
- internal/security/hooks/unicode_posttool.py[169-211]
- internal/runtime/codex_hook_test.go[285-386]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Site preview

Preview: https://f179a026-site.fullsend-ai.workers.dev

Commit: 570cd7f2095e27e6c1b3fce5400924fd568a9c3a

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 10:18 AM UTC · Ended 10:24 AM UTC

Commit: 6722d79 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 10:25 AM UTC · Ended 10:31 AM UTC

Commit: b495cf5 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:32 AM UTC · Completed 11:11 AM UTC

Commit: 1e7ac66 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $8.32

@fullsend-ai-review

Copy link
Copy Markdown

Risk Assessment: moderate (2/5)

Details

Medium-sized follow-up fix to the codex runtime hook with moderate test coverage ratio; high fix/revert churn and change coupling in Tier 2 are counterbalanced by no protected or security-sensitive paths, no dependency changes, and an experienced member author, yielding a moderate overall risk.

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

Medium

  • [edge-case] internal/runtime/codex_hook/fullsend-codex-hook.py:491 — The context-suppression rescan path (lines 491–524) does not return or continue after the rescan passes cleanly. Control falls through to benign_rewrite(verdict["output"]) at line 525, which must also pass for the result to be allowed. A context-suppression-only rewrite always satisfies benign_rewrite today, but the coupling is implicit: if a future chain change adds a new metadata key alongside context_suppressed, benign_rewrite would reject the unknown key and block even though the rescan proved the content safe. This is documented as intentional fail-closed behavior.

Low

  • [pattern-inconsistency] internal/runtime/codex_hook/fullsend-codex-hook.py:406 — Three new helper functions (context_was_suppressed, hook_is_installed, scan_reported_error) lack docstrings, while every other function in this file has one — even one-liners like block(), parse_json(), and claude_tool_name(). The two new classification functions (security_rewrite, benign_rewrite) correctly follow the convention.
    Remediation: Add a single-line docstring to each of the three functions.

  • [test-weakened] internal/runtime/codex_hook_test.go:441TestCodexAdapter_PostToolUseIgnoresContextSuppression assigns input["tool_response"] twice in succession. The first assignment ("many lines of successful test output") is immediately overwritten by the second ("ok example.test 0.5s\nmany safe lines") and is dead code — a leftover from iterating on the test fixture.
    Remediation: Remove the first assignment so only the intended value remains.

  • [naming-convention] internal/runtime/codex_hook/fullsend-codex-hook.py:418scan_reported_error returns a bool but reads as a verb rather than following the predicate pattern used by the other new boolean helpers (security_rewrite, benign_rewrite, context_was_suppressed, hook_is_installed).

  • [pattern-inconsistency] internal/runtime/codex_hook_test.go:253TestCodexAdapter_PostToolUseWithholdsSecretWithTheRealChain manually inlines the same six-entry map that installPostToolChain() encapsulates. Other new tests in this PR (TestCodexAdapter_PostToolUseRescansSuppressedOutputForSecrets, TestCodexAdapter_PostToolUseRescanFindsObfuscatedSecrets, TestCodexAdapter_PostToolUseIgnoresContextSuppression) correctly use h.installPostToolChain().
    Remediation: Replace the inline map with h.installPostToolChain().


Labels: PR modifies security hook behavior in the codex runtime adapter and fixes a category naming issue in the shared unicode posttool hook


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

@fullsend-ai-review fullsend-ai-review 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.

See the review comment for full details.

"fullsend: the previous tool output contained security-sensitive content "
"that codex cannot safely rewrite; the result was withheld"
)
if context_was_suppressed(verdict["output"]):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] edge-case

The context-suppression rescan path (lines 491-524) does not return or continue after the rescan passes cleanly. Control falls through to benign_rewrite(verdict["output"]) at line 525, which must also pass. A context-suppression-only rewrite always satisfies benign_rewrite today, but the coupling is implicit: if a future chain change adds a new metadata key alongside context_suppressed, benign_rewrite would reject the unknown key and block even though the rescan proved the content safe. This is documented as intentional fail-closed behavior.

return bool(metadata.get("unicode_findings")) and bool(categories) and safe_categories


def context_was_suppressed(output: Any) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] pattern-inconsistency

Three new helper functions (context_was_suppressed, hook_is_installed, scan_reported_error) lack docstrings, while every other function in this file has one.

Suggested fix: Add a single-line docstring to each of the three functions.

Comment thread internal/runtime/codex_hook_test.go Outdated

input := codexBashInput("go test ./...")
input["hook_event_name"] = "PostToolUse"
input["tool_response"] = "many lines of successful test output"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] test-weakened

TestCodexAdapter_PostToolUseIgnoresContextSuppression assigns input["tool_response"] twice in succession. The first assignment is immediately overwritten and is dead code.

Suggested fix: Remove the first assignment so only the intended value remains.

return digests is not None and script in digests


def scan_reported_error(output: Any) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] naming-convention

scan_reported_error returns a bool but reads as a verb rather than following the predicate pattern used by the other new boolean helpers (security_rewrite, benign_rewrite, context_was_suppressed, hook_is_installed).

assert.NotContains(t, got.stderr, "sk-live-abcdef", "the flagged value must not be echoed back")
}

func TestCodexAdapter_PostToolUseWithholdsSecretWithTheRealChain(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] pattern-inconsistency

TestCodexAdapter_PostToolUseWithholdsSecretWithTheRealChain manually inlines the same six-entry map that installPostToolChain() encapsulates. Other new tests correctly use h.installPostToolChain().

Suggested fix: Replace the inline map with h.installPostToolChain().

@fullsend-ai-review fullsend-ai-review Bot added the security Security threat model and related concerns label Sep 3, 2026
Assisted-by: Codex (fix), Grok (review)
Signed-off-by: Wayne Sun <gsun@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Finished Review · ❌ Failure (validation failed after 2 iteration(s)) · Started 11:14 AM UTC · Completed 11:56 AM UTC

Commit: 570cd7f · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high

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

Labels

risk/moderate PR risk: moderate security Security threat model and related concerns

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant