fix(codex): register hook wrapper as a python shim#170
Merged
Conversation
Codex runs the registered ~/.codex/hooks/unbound.py as a Python program (its native hook contract — the real hook is `#!/usr/bin/env python3`, and the python installer's Windows branch invokes it as `py -3`). The binary installer wrote a `#!/bin/sh` wrapper into that `.py` path, which is not valid Python, so codex silently dropped it and ran ungoverned while the shell-executed tools (claude-code/cursor/copilot) worked. Write a python shim that os.execv's the binary instead (event read from stdin) — valid whether codex honors the shebang or runs the file through a python interpreter by extension. Update the affected assertions and add regression tests that the generated wrapper is valid Python and execs the binary (and is not a `#!/bin/sh` script). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
|
✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks) 🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head |
Reorder the inline-labelled assertion blocks in test_codex_wrapper_is_valid_python_execing_the_binary so they read (a) valid python -> (b) execs binary -> (c) not a /bin/sh script, matching the docstring's regression narrative. Comment-only reorder; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
🛡️ Automated Security Review (consensus)✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks) Previously acknowledged (not re-flagged)
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head |
pugazhendhi-m
approved these changes
Jun 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Low Risk
Scoped to Codex install-time wrapper file content and tests; no auth, data, or API contract changes beyond fixing broken hook registration.
Overview
Fixes WEB-4850: the binary
setuppath was writing a#!/bin/shscript to~/.codex/hooks/unbound.py, but Codex treats that path as a Python hook. Invalid Python caused Codex to silently drop the hook, so Codex stayed ungoverned on binary installs while shell-invoked tools still worked.The installer now uses
_codex_wrapper_source()to emit a small#!/usr/bin/env python3shim that **os.execv**s intounbound-hook hook codex(stdio unchanged for the stdin hook payload).hooks.jsonstill registers the bare wrapper path; only the on-disk wrapper content changes.Tests add WEB-4850 regression coverage (
compileas Python,os.execv, no#!/bin/sh) and update migration/setup assertions for the new shim.Reviewed by Cursor Bugbot for commit 499126b. Bugbot is set up for automated code reviews on this repo. Configure here.
Greptile Summary
Fixes WEB-4850: the binary installer was writing a
#!/bin/shscript to~/.codex/hooks/unbound.py, but Codex executes that file as Python by extension, silently dropping the hook (fail-open). The fix replaces the shell script with a valid Python shim that usesos.execvto hand off to the binary, keeping stdin/stdout/stderr intact for the hook payload._codex_wrapper_source()generates a three-line Python shim (#!/usr/bin/env python3+import os+os.execv(...)) with the binary path safely embedded viarepr(), andO_TRUNCensures re-runs overwrite any stale#!/bin/shwrapper.compile()on_codex_wrapper_source()directly, and an e2e test asserts the file written bysetup_cmd.run()is valid Python, executable, and registered by bare path inhooks.json.Confidence Score: 5/5
Safe to merge — the change is surgical, well-tested, and only affects Codex hook installation; other tools are untouched.
The fix correctly replaces a shell script with a minimal Python shim that Codex can actually execute.
repr()safely handles any special characters in the binary path.O_TRUNCensures old#!/bin/shwrappers are overwritten on re-run. The three new/updated test assertions — including acompile()call on both the in-memory source and the on-disk file — directly reproduce the failure mode and lock it in. No auth, data, or multi-tenant paths are touched.No files require special attention.
Important Files Changed
_codex_wrapper_source()to generate a valid Python shim and updates_install_codex_hooks_for_user()to write it;repr()safely embeds the binary path,O_TRUNChandles migration of old sh wrappers, andos.execvpreserves stdin/stdout/stderr for the hook payload.#!/bin/sh/execchecks to#!/usr/bin/env python3/os.execvchecks, and adds two new tests that usecompile()to lock in the Python-validity invariant end-to-end.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["setup_cmd.run()"] --> B["_install_codex_hooks_for_user()"] B --> C["_install() — privilege-dropped, runs as user"] C --> D["hooks_dir.mkdir(parents=True, exist_ok=True)"] D --> E["os.open(wrapper, O_WRONLY|O_CREAT|O_TRUNC|O_NOFOLLOW, 0o755)"] E --> F["f.write(_codex_wrapper_source())"] F --> G["os.chmod(wrapper, 0o755)"] G --> H["_merge_codex_hooks_json(hooks.json, bare_wrapper_path)"] H --> I["Done — ~/.codex/hooks/unbound.py is valid Python shim"] subgraph shim["Generated unbound.py shim (at hook runtime)"] J["#!/usr/bin/env python3"] --> K["import os"] K --> L["os.execv(HOOK_BINARY, ['unbound-hook','hook','codex'])"] L --> M["Binary inherits stdin/stdout/stderr — hook payload passes through"] end I -.->|"Codex runs as Python"| shim%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A["setup_cmd.run()"] --> B["_install_codex_hooks_for_user()"] B --> C["_install() — privilege-dropped, runs as user"] C --> D["hooks_dir.mkdir(parents=True, exist_ok=True)"] D --> E["os.open(wrapper, O_WRONLY|O_CREAT|O_TRUNC|O_NOFOLLOW, 0o755)"] E --> F["f.write(_codex_wrapper_source())"] F --> G["os.chmod(wrapper, 0o755)"] G --> H["_merge_codex_hooks_json(hooks.json, bare_wrapper_path)"] H --> I["Done — ~/.codex/hooks/unbound.py is valid Python shim"] subgraph shim["Generated unbound.py shim (at hook runtime)"] J["#!/usr/bin/env python3"] --> K["import os"] K --> L["os.execv(HOOK_BINARY, ['unbound-hook','hook','codex'])"] L --> M["Binary inherits stdin/stdout/stderr — hook payload passes through"] end I -.->|"Codex runs as Python"| shimReviews (3): Last reviewed commit: "Merge branch 'main' of https://github.co..." | Re-trigger Greptile