Profile config causes global hooks.json hooks to be discovered and executed twice
Summary
When launching Codex with a v2 profile (for example codex --profile gpt), every global hook in ~/.codex/hooks.json is discovered and executed twice.
This is not only duplicated UI output: the hook command is actually spawned twice.
Environment
- Codex CLI:
codex-cli 0.135.0 - Platform: macOS
- Launch command:
codex --profile gpt - Global hooks file:
~/.codex/hooks.json - Profile file:
~/.codex/gpt.config.toml
Reproduction
- Create a base user config:
``toml``
# ~/.codex/config.toml
model = "gpt-5.5"
- Create a v2 profile in the same Codex home:
``toml``
# ~/.codex/gpt.config.toml
model = "gpt-5.5"
- Add a global hook in
~/.codex/hooks.json, for example aUserPromptSubmitorSessionStartcommand that appends its PID to a temp file.
- Start Codex with the profile:
``bash``
codex --profile gpt
# or
codex exec --profile gpt ...
Observed behavior
The same hook is run twice for a single event.
In my verification, I temporarily added a probe to an existing UserPromptSubmit hook script. A single user prompt produced two log lines with different PIDs:
hook: UserPromptSubmit
hook: UserPromptSubmit
hook: UserPromptSubmit Completed
hook: UserPromptSubmit Completed
matching_lines 2
1: avm-existing-hook-proof-1780331979993 pid=77018
2: avm-existing-hook-proof-1780331979993 pid=77019
The same duplicated behavior was also visible for other hook events, including SessionStart, PreToolUse, PostToolUse, and Stop.
Expected behavior
A global ~/.codex/hooks.json hook should be discovered and executed once per matching event, even when a profile is active.
Suspected cause
The profile appears to create an additional user config layer. Both the base user config and profile config live under the same Codex home, so both layers resolve hooks_config_folder() to the same directory (~/.codex). Hook discovery then loads the same ~/.codex/hooks.json once per layer and does not deduplicate by source path.
Relevant code path appears to be in codex-rs/hooks/src/engine/discovery.rs:
for layer in config_layer_stack.get_layers(...) {
let json_hooks = load_hooks_json(layer.hooks_config_folder().as_deref(), &mut warnings);
...
for (source_path, hook_events) in [json_hooks, toml_hooks].into_iter().flatten() {
append_hook_events(...);
}
}
Because both user layers have the same hooks config folder, the same hooks.json source path is appended twice.
Impact
This doubles the runtime cost of hooks when using profiles. It is especially noticeable for hooks that:
- perform memory/vector recall on
UserPromptSubmit, - run before/after every tool call (
PreToolUse/PostToolUse), - write logs or index data,
- inject additional context into the model.
It can also duplicate model-visible hook context, increasing token usage and potentially causing repeated or overweighted context.
Possible fixes
- Deduplicate loaded hook declaration sources by canonical
source_pathduring hook discovery; or - Treat profile user layers as config overrides that do not independently rediscover sibling
hooks.json; or - Explicitly load global
~/.codex/hooks.jsononce per effective config stack.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗