Skip to content

fix(codex): register hook wrapper as a python shim#170

Merged
MohamedAklamaash merged 3 commits into
mainfrom
web-4919
Jun 23, 2026
Merged

fix(codex): register hook wrapper as a python shim#170
MohamedAklamaash merged 3 commits into
mainfrom
web-4919

Conversation

@MohamedAklamaash

@MohamedAklamaash MohamedAklamaash commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

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 setup path was writing a #!/bin/sh script 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 python3 shim that **os.execv**s into unbound-hook hook codex (stdio unchanged for the stdin hook payload). hooks.json still registers the bare wrapper path; only the on-disk wrapper content changes.

Tests add WEB-4850 regression coverage (compile as 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/sh script 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 uses os.execv to 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 via repr(), and O_TRUNC ensures re-runs overwrite any stale #!/bin/sh wrapper.
  • Two new unit tests lock in the regression: one calls compile() on _codex_wrapper_source() directly, and an e2e test asserts the file written by setup_cmd.run() is valid Python, executable, and registered by bare path in hooks.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_TRUNC ensures old #!/bin/sh wrappers are overwritten on re-run. The three new/updated test assertions — including a compile() 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

Filename Overview
binary/src/unbound_hook/setup_cmd.py Adds _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_TRUNC handles migration of old sh wrappers, and os.execv preserves stdin/stdout/stderr for the hook payload.
binary/tests/test_setup_migration.py Updates three existing test assertions from #!/bin/sh / exec checks to #!/usr/bin/env python3 / os.execv checks, and adds two new tests that use compile() 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
Loading
%%{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"| shim
Loading

Reviews (3): Last reviewed commit: "Merge branch 'main' of https://github.co..." | Re-trigger Greptile

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>
@MohamedAklamaash MohamedAklamaash requested a review from a team June 23, 2026 06:51
Comment thread binary/tests/test_setup_migration.py
@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)


🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head e24cb18c · 2026-06-23T07:00Z

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>
@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

🛡️ Automated Security Review (consensus)

✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)

Previously acknowledged (not re-flagged)

  • 0o755 on Codex hook wrapper (setup_cmd.py:269, setup_cmd.py:301) — Semgrep insecure-file-permissions; maintainer prior consensus: no issues found (@vigneshsubbiah16, 2026-06-23). Executable user-owned shim in ~/.codex/hooks/ requires the execute bit; not a meaningful privilege expansion beyond the prior shell wrapper.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head dfb6e88a · 2026-06-23T07:11Z

@MohamedAklamaash MohamedAklamaash merged commit 61f6c3d into main Jun 23, 2026
4 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.

3 participants