Two Codex bugs, two pull requests, and a lesson in scope
I originally started looking into Caveman's Codex integration because something didn't add up. Running codex login appeared to work normally, but the native agent could still end up using the wrong authentication route. I wanted to understand where that mismatch was coming from.
It took a bit of tracing because there were several pieces involved. Codex maintained its own authentication state, Caveman had configuration describing how requests should be routed, and the native agent had to pick up the correct settings when a session started. These things didn't necessarily update together.
The request path looked roughly like this:
codex login
|
v
Codex authentication state
|
+---- Caveman routing configuration
|
v
Native agent startup
|
v
SessionStart
|
v
Authentication route selection
|
v
Proxy
The important question wasn't whether codex login succeeded. It was whether the native agent was using the authentication state that Codex actually had when the session started.
Following the authentication state
The issue became clearer when I looked at what happened after a user changed their login.
Caveman could have already written its Codex configuration while the user was authenticated with an API key. Then the user would run codex login and switch to a ChatGPT subscription. Codex would have the new authentication state, but Caveman's routing configuration could still contain the previous route.
That meant the configuration could become stale without anything immediately failing. The problem would only show up when the native agent started making requests.
I proposed handling this in PR #1047 by checking the current authentication state during SessionStart and repairing the routing configuration if it no longer matched.
There was a catch, though. Caveman already had caveman doctor codex --fix for detecting and repairing stale configuration. Having a startup hook automatically rewrite ~/.codex/config.toml was a separate decision. It might be convenient, but it also meant modifying a user's configuration without them explicitly asking for a repair.
That distinction came up during review, and it eventually became important to the rest of this work.
A separate problem with the API-key route
While tracing the authentication flow, I also found that the API-key route itself was incorrect.
Caveman was configuring Codex with /w/codex, but the proxy expected the OpenAI Responses endpoint under /w/codex/v1. The difference becomes clear when you look at the paths the client actually constructs.
The old configuration produced:
/w/codex
+
/responses
|
v
/w/codex/responses
What the proxy needed was:
/w/codex/v1
+
/responses
|
v
/w/codex/v1/responses
Without that /v1 segment, the request didn't match the OpenAI adapter and returned 404 cave_route_not_found before it could reach OpenAI.
There was also a problem with the existing conformance test. Its Codex stub was adding /v1/responses to the configured base URL, while the real Codex client was adding only /responses. The test was effectively supplying the missing path segment itself, so the incorrect configuration still produced a valid-looking request.
I changed the stub to match the real client's behaviour. Once that was done, the test reproduced the 404 immediately.
The fix introduced a shared constant:
CODEX_API_KEY_ROUTE = "/w/codex/v1"
The same value is now used by the provider configuration, installation journal and nativeIntegrationStatus. Existing installations with the old route can also be identified as degraded, with caveman doctor codex --fix providing the repair path.
The subscription route remains /chatgpt, since it uses a different proxy handler and doesn't need the /v1 suffix.
This part of the work went into PR #1015, which carried the routing correction and the conformance-test changes. Commit 8bc01d6 introduced the corrected route constant, and I received co-author credit for the contribution.
Another issue in the Codex hook
A little later, I went back to Caveman's Codex integration for a different issue. This one came from issue #185.
Caveman injects its instructions when a coding-agent session starts. The Claude integration used a shared configuration resolver to decide which rules to load, but the Codex integration had a much simpler implementation. Its .codex/hooks.json contained a SessionStart command that echoed the entire ruleset as a static string.
In practice, it was just:
Codex session starts
|
v
SessionStart hook
|
v
echo the full ruleset
There was no configuration lookup or mode selection. CAVEMAN_DEFAULT_MODE was ignored, repository-local .caveman/config.json files were ignored, and the user's defaultMode setting had no effect. Even setting the mode to off didn't stop the rules from being injected.
The Claude integration already had the necessary logic in src/hooks/caveman-config.js. It resolved the mode in this order: environment variable, repository configuration, user configuration and finally the built-in default. Codex simply wasn't using it.
I didn't want to add another implementation of that logic. The whole point of having a shared resolver was to avoid maintaining separate configuration behaviour for different integrations.
So PR #1139 added .codex/codex-sessionstart.js, which uses the existing resolver and loads the filtered rules from the same SKILL.md source as the Claude integration.
The hook only reads configuration and prints the selected instructions. It doesn't modify user files or try to repair configuration automatically.
There were a few other details worth handling. The hook needed to exit successfully without output when the selected mode was off, preserve special modes such as commit, review and compress, and keep the canonical wenyan-full label. It also needed a fallback resolver for installations where the shared module wasn't available beside the hook. I added a test to make sure that fallback's valid-mode list stays aligned with the real VALID_MODES export.
I made the hook fail open as well, so a configuration or file-loading error wouldn't prevent Codex from starting. One small implementation detail was avoiding process.exit() immediately after process.stdout.write(). The process needs to finish naturally so buffered output isn't lost. There's a test that pipes the output through another process and checks that the complete result arrives.
Finally, Codex hooks run from the session's working directory, which might be a subdirectory of the repository. The manifest therefore resolves the Git root before invoking the script:
node "$(git rev-parse --show-toplevel)/.codex/codex-sessionstart.js"
That way, the hook doesn't depend on where the user happened to launch Codex.
Testing the changes
PR #1139 added 15 tests, all using isolated fixture copies of .codex, src and skills. That was important because I didn't want the results depending on configuration or files installed on my own machine.
The tests cover configuration precedence, environment and repository settings, user configuration, off, the special modes, invalid JSON, symlinked configuration files, fallback behaviour, execution from subdirectories, the manifest command and stdout flushing.
I also checked that all 15 tests failed against the unfixed implementation before considering the change complete. The manifest test was particularly useful because it caught the original static echo command rather than simply checking the behaviour of the new implementation.
The full repository suite had 419 tests: 394 passed, 23 were skipped and two failed. Both failures also occurred on unmodified main under Windows, due to unprivileged symlink creation and a missing PowerShell executable.
I ran the relevant runtime checks manually too. The default configuration produced the full ruleset, CAVEMAN_DEFAULT_MODE=off produced nothing, the other modes selected the expected instructions, and running from a subdirectory worked as expected. The native-enable runtime suite also passed 44/44, alongside the TypeScript builds and root-level checks.
How the two pull requests ended up differently
The two contributions went through rather different review outcomes.
The routing correction from the authentication investigation was adopted in PR #1015. The proposed automatic repair behaviour from #1047 wasn't accepted in its original form. The maintainer's reasoning was that caveman doctor codex --fix already provided a way to repair stale configuration, and a startup hook shouldn't necessarily rewrite ~/.codex/config.toml without the user's permission. A hook that warns about the mismatch would be a narrower alternative.
I agreed with that reasoning. The routing bug needed fixing, but automatically changing a user's configuration was not necessary to fix it.
PR #1139 had a different outcome. Julius adopted the work into PR #1103 and cherry-picked it, preserving my authorship. The review specifically called out the shared resolver, the stdout handling, the fallback whitelist test and the fact that the degraded path still respected the full configuration precedence.
There was also one small addition from the maintainer: a row in CLAUDE.md's single-source table, since that file documents behaviour and the table didn't mention the new hook.
The adopted commit was:
635253b feat(codex): SessionStart hook honors defaultMode
Author: g0GobliN
Committer: Claude
The author and committer being different is intentional. The maintainer's tooling cherry-picked the commit into the triage branch, changing the committer while preserving the original author.
I found that distinction interesting because the two PRs involved the same project and maintainer, but had different outcomes. #1015 carried my routing work with co-author credit, while #1139 was adopted with the original commit and authorship intact.
Where things stood
As of October 1, the second change was included as a credited commit in the daily triage PR #1103. CI was green, but the PR hadn't merged yet. PR #1139 was superseded by that work, and issue #185 was waiting for the integration to land.
Looking back at the two investigations, the technical problems were quite different. One involved authentication state and proxy routing; the other was about making two integrations use the same configuration logic. But both required following the actual behaviour rather than relying on what the existing tests appeared to prove.
The part I found most useful was the review of #1047. I had a working idea for automatically fixing stale configuration, but that didn't automatically make it the right behaviour for a startup hook. The existing repair command was already there, and silently rewriting a user's configuration introduced a different responsibility.
That changed how I approached #1139. I kept the hook limited to reading the configuration and producing the right output. It didn't need to own anything else.
I'm still learning how to contribute effectively to projects I don't maintain myself. These two PRs gave me a much better understanding of the difference between finding a real problem, implementing a fix, and choosing how much responsibility that fix should take on.