You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Codex CLI renders hook additionalContext as visible developer message #16933
Codex CLI is rendering hook hookSpecificOutput.additionalContext as a visible developer message in the session transcript.
This appears to contradict the documented behavior used by the Hindsight Codex integration, which says auto-recall should be "invisible to the transcript, visible to Codex".
I rely on UserPromptSubmit.additionalContext to inject strong behavioral guidance on every turn. Behaviorally, this works well. The problem is almost entirely presentation:
Codex renders the full hook-provided context into the visible transcript every turn
that crowds the terminal window and makes live sessions harder to read
it creates pressure to shorten or externalize otherwise-good hook contracts purely to reduce UI noise
So from this side, the issue is not "hooks are too verbose". The issue is that Codex currently makes hook context too visible.
Ideal behavior would be one of:
keep additionalContext model-visible but transcript-invisible
or show a collapsed one-line marker like UserPromptSubmit hook applied
or make hook visibility configurable, for example hidden | collapsed | full
optionally suppress repeated identical hook context and show only a small marker/diff
If Codex hid or collapsed this automatically, the current hook architecture would be fine. The operational behavior is already good; the transcript rendering is the thing pushing users toward worse designs.
I dug into this locally and I think the root cause is pretty narrow.
hookSpecificOutput.additionalContext is correctly being turned into model-visible developer context, but it currently goes through record_conversation_items in codex-rs/core/src/hook_runtime.rs. That path emits/persists a raw transcript item, which is why integrations like Hindsight end up showing the full memory block as a visible developer message.
The behavior I think would solve this cleanly is:
Keep additionalContext model-visible.
Record it into in-memory history only, not the durable transcript/event stream.
Make the hook output visibility configurable on the command hook itself, for example:
additionalContextVisibility = "collapsed"# hidden | collapsed | full
Where:
hidden injects the context for the model but renders no hook output entry.
collapsed injects the context and renders a small marker like additional context applied.
full preserves the current visible/full-context behavior for users who want that.
I have a local patch along those lines with regression coverage. It changes the additional-context recording path to use in-memory history instead of record_conversation_items, adds the per-hook visibility enum/config, updates generated config/app-server schemas, and covers the hidden/collapsed/full behavior in hook tests.
Validation run locally:
just fmt
just write-config-schema
just write-app-server-schema
cargo test -p codex-config
cargo test -p codex-hooks
cargo test -p codex-core hook_runtime::tests::record_additional_contexts_stays_out_of_transcript_events
cargo test -p codex-app-server-protocol
cargo test -p codex-app-server config_api::tests::map_requirements_toml_to_api_converts_core_enums
just fix -p codex-config
just fix -p codex-hooks
just fix -p codex-core
just fix -p codex-app-server-protocol
just fix -p codex-app-server
git diff --check
Happy to open a PR if this approach matches the intended hook contract.
Thanks for the detailed discussion here — I dug into the Codex path and I believe there are two separable concerns:
the minimal correctness fix, and
an optional UI/config visibility behavior.
The narrow bug appears to be in codex-rs/core/src/hook_runtime.rs, where hookSpecificOutput.additionalContext is converted into developer-role ResponseItems and then passed through record_conversation_items.
That helper currently:
appends items to in-memory conversation history,
persists them into rollout/session-log state,
emits raw response-item events for app-server/v2 clients.
For normal conversation items, that is correct. For hook additionalContext, this seems too broad.
If the contract is “model-visible, transcript-invisible,” model visibility is already satisfied by adding these items to in-memory history. The persistence/event emission side effects are what make this content appear in transcript/session-log surfaces.
Minimal fix
Update record_additional_contexts to record only into in-memory history:
(Important detail: parameter order differs between these APIs.)
This preserves the important behavior:
model still receives hook-provided additionalContext
no rollout/session-log persistence for hidden context
no RawResponseItem emission for app-server/v2 transcript rendering
existing developer-role conversion can remain unchanged
Regression test shape
A good regression test would assert:
additional context is present in the in-memory conversation state used for the next model request,
absent from rollout/session persistence,
absent from raw response-item notifications.
Optional follow-up (UX/config)
A configurable rendering mode could still be useful, e.g.:
additionalContextVisibility = "collapsed"# hidden | collapsed | full
Where:
hidden: inject for model, render nothing
collapsed: inject for model, render a small marker (e.g. “additional context applied”)
full: preserve current fully visible behavior
But I’d treat this as a follow-up UX/config layer. The core issue is that additionalContext currently flows through the same recording path as transcript-visible conversation items.
Adding one more concrete large-context use case and validation point.
Environment:
Codex CLI: codex-cli 0.128.0
Platform: Darwin 25.4.0 arm64 arm
Terminal: Ghostty 1.3.1
Surface: standalone Codex CLI with UserPromptSubmit hooks
Use case:
A project-level agent harness needs to inject a large session-start context block into the model. The payload is not ordinary chat content; it is source/context material such as:
project documentation selected by a read plan
source-pack style context
read receipts / audit metadata
startup context needed for the model to behave correctly in a new session
The desired UX is:
the model receives the full context
the user sees only a compact audit/status report
the CLI transcript is not flooded by hundreds of KB of hook context
Current behavior:
UserPromptSubmit.hookSpecificOutput.additionalContext is both model-visible and transcript-visible. In a real harness test, a new-session hook returned a compact report plus a source-pack-like context payload. Codex accepted the hook output, but the entire payload was rendered into the visible CLI transcript as hook/developer context.
If the harness suppresses the large additionalContext to avoid flooding the CLI, the model no longer receives the full session-start context. That creates a hard tradeoff:
full model context + fast startup + transcript flood
clean transcript + fast startup + incomplete model context
clean transcript + complete model context + slow/manual fallback reads
This is exactly the kind of case where a model-visible but hidden/collapsed hook context channel is needed.
Minimal internal fix if the intended contract is already “model-visible, transcript-invisible”:
Record hook-provided additionalContext into the in-memory model history used for the next request, but do not persist/emit it through the same transcript/session-log/raw-response-item path as normal visible conversation items unless visibility is explicitly full.
This would preserve the important behavior:
the model still receives additionalContext
the terminal UI remains readable
integrations do not have to choose between correctness and usability
users who want full hook auditability can opt into full
This issue is not about hiding tool execution from users. A compact marker or audit line is still useful. The problem is that the full model-context payload is currently forced into the human-facing transcript.
Indexed this hook ticket in the umbrella tracker: #21753
Goal: collect the scattered Codex hook requests and bugs into one parity matrix for Full Claude Code Hook Parity (29+), while preserving this issue as the detailed thread for its specific behavior.
I prepared a small patch for this in a fork because direct PR creation against openai/codex is currently blocked for my account (CreatePullRequest permission denied via GraphQL; REST returned 404).
The dangerous part is not only that �dditionalContext becomes visible, it is that the receipt boundary collapses. Hidden runtime context and visible transcript context serve different jobs, and once they merge, integrations can no longer rely on either privacy or clean postmortems. I would want Codex to keep a separate provenance tag for hook-provided hidden context so the model can consume it while the operator can still audit that it existed without seeing it rendered as a normal developer message.
I can reproduce this same class of issue with a Superpowers SessionStart hook setup.
Superpowers returns hookSpecificOutput.additionalContext so the model receives startup instructions. Today Codex surfaces that context through two separate visible paths:
HookCompleted entries as HookOutputEntryKind::Context, which the TUI renders as hook context: ...
persisted/raw response items because record_additional_contexts currently uses record_conversation_items, making the context transcript-visible
The approach is intentionally narrower than a fold/collapsed UI feature:
keep hook additionalContext model-visible
record it into in-memory model history only, not rollout/raw transcript output
honor suppressOutput: true for SessionStart, SubagentStart, and UserPromptSubmit context hook entries
keep stop/block/error feedback visible
leave PreToolUse / PostToolUsesuppressOutput behavior unchanged for now
A direct PR against openai/codex is currently blocked for my account with CreatePullRequest permission denied, so I pushed the patch to a fork and linked the compare above.
Maintainer questions:
Is the intended contract for additionalContext model-visible but transcript/raw-response-hidden?
Is it acceptable to support suppressOutput first for SessionStart / SubagentStart / UserPromptSubmit, while leaving the currently unsupported tool-hook cases alone?
Should the default HookCompleted context entry stay fully visible unless suppressOutput: true, or would maintainers prefer hidden-by-default for all additionalContext?
Thanks for documenting this. I took a quick look and wanted to share a possible direction, in case it helps.
It looks like the core issue may be that hook additionalContext is currently flowing through the same path that records conversation items and emits raw response items for the visible transcript.
One possible fix direction could be to keep those hook-provided developer messages available to the model, but avoid emitting them through the raw response item path that the visible transcript consumes.
Concretely, that would mean:
keep additionalContext in the model-visible conversation history
avoid rendering those hook-injected messages as visible transcript items
add a regression test confirming that hook context updates history without producing visible raw item events
I tried a small local patch in that direction and the focused hook runtime tests passed locally:
cargo fmt --check
cargo test -p codex-core hook_runtime::tests --lib
I hit the same blocker while porting lore's Claude Code integration to Codex.
Context: lore injects deterministic coding conventions through hook hookSpecificOutput.additionalContext. On Claude Code this is model-visible but not displayed in the
operator transcript, which is essential: the injected payload can be full pattern files/pages of
project conventions.
I built a local Codex plugin/adapter and verified:
Codex local plugin install works
bundled skills load
bundled MCP config loads
hooks fire
SessionStart, PreToolUse, UserPromptSubmit, and PostToolUse fixtures are accepted
additionalContext reaches the model
But the UX is not shippable because Codex renders the injected context visibly in the terminal. This means pages of hook-injected convention text are interleaved with normal agent output, making it hard for the operator to distinguish injected context from conversation.
For this integration, summarizing/truncating is not a viable workaround. The product requirement is full-fidelity, deterministic, model-visible context that is hidden from the operator transcript, as Claude Code provides.
That bug reproduces SessionStart hooks for source=compact firing on later turns when no new compaction actually took place, due to duplicate compact events being drained later. If a hook injects additionalContext, that can cause extra unexpected developer-message/context pollution on a later turn, which looks directly relevant to visible reinjection symptom here.
I prepared a focused patch against current main at 8268cbfb0e5f39cb4efff928264fe8f29ddacafb.
The root cause is that record_additional_contexts currently calls record_conversation_items, which does three things at once: updates model history, persists the item to the rollout, and emits RawResponseItem. Hook context needs only the first behavior.
This patch:
keeps hook additionalContext in the in-memory history sent to the model;
stops persisting/emitting the full developer message as a transcript item;
honors suppressOutput: true for the UserPromptSubmit context entry, while keeping stop/block/error feedback visible;
adds regression coverage for model visibility, transcript-event silence, and suppressOutput;
makes no config or schema changes.
With the patch, a hook can request completely silent context injection with:
diff --git a/codex-rs/core/src/hook_runtime.rs b/codex-rs/core/src/hook_runtime.rs
index 342e4d4..a42169d 100644
--- a/codex-rs/core/src/hook_runtime.rs+++ b/codex-rs/core/src/hook_runtime.rs@@ -602,7 +602,7 @@ pub(crate) async fn record_additional_contexts(
return;
}
- sess.record_conversation_items(turn_context, developer_messages.as_slice())+ sess.record_model_context_items(turn_context, developer_messages.as_slice())
.await;
}
@@ -788,7 +788,9 @@ mod tests {
use super::additional_context_messages;
use super::hook_run_analytics_payload;
use super::hook_run_metric_tags;
+ use super::record_additional_contexts;
use crate::session::tests::make_session_and_context;
+ use crate::session::tests::make_session_and_context_with_rx;
use codex_protocol::protocol::HookCompletedEvent;
use codex_protocol::protocol::HookRunSummary;
use codex_utils_absolute_path::test_support::PathBufExt;
@@ -828,6 +830,36 @@ mod tests {
);
}
+ #[tokio::test]+ async fn record_additional_contexts_stays_out_of_transcript_events() {+ let (session, turn_context, events) = make_session_and_context_with_rx().await;+ let additional_context = "transcript-hidden reef";++ record_additional_contexts(+ &session,+ &turn_context,+ vec![additional_context.to_string()],+ )+ .await;++ let history = session.clone_history().await;+ let [codex_protocol::models::ResponseItem::Message { role, content, .. }] =+ history.raw_items()+ else {+ panic!("expected one model-visible developer message");+ };+ assert_eq!(+ (role.as_str(), content.as_slice()),+ (+ "developer",+ &[ContentItem::InputText {+ text: additional_context.to_string(),+ }][..],+ )+ );+ assert!(events.try_recv().is_err());+ }+
#[tokio::test]
async fn hook_run_analytics_payload_uses_completed_turn_id() {
let (_session, turn_context) = make_session_and_context().await;
diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs
index 560dcf5..c140764 100644
--- a/codex-rs/core/src/session/mod.rs+++ b/codex-rs/core/src/session/mod.rs@@ -2711,8 +2711,7 @@ impl Session {
}
}
- /// Records conversation items: append to history, persist to rollout, and- /// notify clients observing raw response items.+ /// Normalizes conversation items before adding them to history.
pub(crate) fn prepare_conversation_items_for_history<'a>(
&self,
turn_context: &TurnContext,
@@ -2773,6 +2772,31 @@ impl Session {
))
}
+ async fn record_prepared_items_in_history(+ &self,+ turn_context: &TurnContext,+ items: &[ResponseItem],+ ) {+ let mut state = self.state.lock().await;+ state.current_time_reminder.note_recorded_items(items);+ state.record_items(+ items.iter(),+ turn_context.model_info.truncation_policy.into(),+ );+ }++ /// Records model-only context without persisting it or notifying clients.+ #[tracing::instrument(level = "trace", skip_all, fields(item_count = items.len()))]+ pub(crate) async fn record_model_context_items(+ &self,+ turn_context: &TurnContext,+ items: &[ResponseItem],+ ) {+ let items = self.prepare_conversation_items_for_history(turn_context, items);+ self.record_prepared_items_in_history(turn_context, items.as_ref())+ .await;+ }+
#[tracing::instrument(level = "trace", skip_all, fields(item_count = items.len()))]
pub(crate) async fn record_conversation_items(
&self,
@@ -2781,14 +2805,8 @@ impl Session {
) {
let items = self.prepare_conversation_items_for_history(turn_context, items);
let items = items.as_ref();
- {- let mut state = self.state.lock().await;- state.current_time_reminder.note_recorded_items(items);- state.record_items(- items.iter(),- turn_context.model_info.truncation_policy.into(),- );- }+ self.record_prepared_items_in_history(turn_context, items)+ .await;
self.persist_rollout_response_items(items).await;
self.send_raw_response_items(turn_context, items).await;
}
diff --git a/codex-rs/hooks/src/events/user_prompt_submit.rs b/codex-rs/hooks/src/events/user_prompt_submit.rs
index 2934bd3..b9609a9 100644
--- a/codex-rs/hooks/src/events/user_prompt_submit.rs+++ b/codex-rs/hooks/src/events/user_prompt_submit.rs@@ -156,6 +156,7 @@ fn parse_completed(
} else if let Some(parsed) =
output_parser::parse_user_prompt_submit(&run_result.stdout)
{
+ let suppress_output = parsed.universal.suppress_output;
if let Some(system_message) = parsed.universal.system_message {
entries.push(HookOutputEntry {
kind: HookOutputEntryKind::Warning,
@@ -165,13 +166,16 @@ fn parse_completed(
if parsed.invalid_block_reason.is_none()
&& let Some(additional_context) = parsed.additional_context
{
- common::append_additional_context(- &mut entries,- &mut additional_contexts_for_model,- additional_context,- );+ if suppress_output {+ additional_contexts_for_model.push(additional_context);+ } else {+ common::append_additional_context(+ &mut entries,+ &mut additional_contexts_for_model,+ additional_context,+ );+ }
}
- let _ = parsed.universal.suppress_output;
if !parsed.universal.continue_processing {
status = HookRunStatus::Stopped;
should_stop = true;
@@ -288,6 +292,30 @@ mod tests {
use crate::engine::ConfiguredHandler;
use crate::engine::command_runner::CommandRunResult;
+ #[test]+ fn suppress_output_hides_context_entry() {+ let parsed = parse_completed(+ &handler(),+ run_result(+ Some(0),+ r#"{"suppressOutput":true,"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"hidden prompt context"}}"#,+ "",+ ),+ Some("turn-1".to_string()),+ );++ assert_eq!(+ parsed.data,+ UserPromptSubmitHandlerData {+ should_stop: false,+ stop_reason: None,+ additional_contexts_for_model: vec!["hidden prompt context".to_string()],+ }+ );+ assert_eq!(parsed.completed.run.status, HookRunStatus::Completed);+ assert_eq!(parsed.completed.run.entries, Vec::new());+ }+
#[test]
fn continue_false_preserves_context_for_later_turns() {
let parsed = parse_completed(
Red/green validation:
Before the production change, suppress_output_hides_context_entry failed because a full Context entry was emitted.
Before the production change, record_additional_contexts_stays_out_of_transcript_events failed because events.try_recv() received a raw response item.
just test -p codex-hooks: 121 passed.
just test -p codex-core hook_runtime::tests: 6 passed.
just fmt: passed.
git diff --check: passed.
I did not run the complete workspace test suite; the results above are the focused affected-crate suites. I have not opened an upstream PR because docs/contributing.md says external PRs are invitation-only. If a maintainer confirms this direction, I can package the same patch as an invited PR.
This seems to be fixed for me in v0.143.0. The output still renders to the user but using … +1044 lines (ctrl + t to view transcript) to hide long output.
For anyone tracking this: the two upstream attempts at a fix (the UserInstructions stack#30138–#30141 and the async hook stack#31885–#31887) were both closed unmerged in the week of 2026-07-06, and the community patch above is still awaiting review.
I've asked for maintainer direction on the intended design over in #21696, since that's where the design options are laid out. This bug and that feature request resolve the same underlying coupling of model visibility and TUI rendering.
Activity
github-actions commentedon Apr 6, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
UserPromptSubmit.additionalContextas multiline hook context in the TUI #16486Powered by Codex Action
Nickonomic commentedon Apr 22, 2026
Adding another concrete use case here.
I rely on
UserPromptSubmit.additionalContextto inject strong behavioral guidance on every turn. Behaviorally, this works well. The problem is almost entirely presentation:So from this side, the issue is not "hooks are too verbose". The issue is that Codex currently makes hook context too visible.
Ideal behavior would be one of:
additionalContextmodel-visible but transcript-invisibleUserPromptSubmit hook appliedhidden | collapsed | fullIf Codex hid or collapsed this automatically, the current hook architecture would be fine. The operational behavior is already good; the transcript rendering is the thing pushing users toward worse designs.
NicholaiVogel commentedon Apr 24, 2026
I dug into this locally and I think the root cause is pretty narrow.
hookSpecificOutput.additionalContextis correctly being turned into model-visible developer context, but it currently goes throughrecord_conversation_itemsincodex-rs/core/src/hook_runtime.rs. That path emits/persists a raw transcript item, which is why integrations like Hindsight end up showing the full memory block as a visibledevelopermessage.The behavior I think would solve this cleanly is:
additionalContextmodel-visible.Where:
hiddeninjects the context for the model but renders no hook output entry.collapsedinjects the context and renders a small marker likeadditional context applied.fullpreserves the current visible/full-context behavior for users who want that.I have a local patch along those lines with regression coverage. It changes the additional-context recording path to use in-memory history instead of
record_conversation_items, adds the per-hook visibility enum/config, updates generated config/app-server schemas, and covers the hidden/collapsed/full behavior in hook tests.Validation run locally:
Happy to open a PR if this approach matches the intended hook contract.
aaf2tbz commentedon Apr 24, 2026
Thanks for the detailed discussion here — I dug into the Codex path and I believe there are two separable concerns:
The narrow bug appears to be in
codex-rs/core/src/hook_runtime.rs, wherehookSpecificOutput.additionalContextis converted into developer-roleResponseItems and then passed throughrecord_conversation_items.That helper currently:
For normal conversation items, that is correct. For hook
additionalContext, this seems too broad.If the contract is “model-visible, transcript-invisible,” model visibility is already satisfied by adding these items to in-memory history. The persistence/event emission side effects are what make this content appear in transcript/session-log surfaces.
Minimal fix
Update
record_additional_contextsto record only into in-memory history:instead of:
(Important detail: parameter order differs between these APIs.)
This preserves the important behavior:
additionalContextRawResponseItememission for app-server/v2 transcript renderingRegression test shape
A good regression test would assert:
Optional follow-up (UX/config)
A configurable rendering mode could still be useful, e.g.:
Where:
hidden: inject for model, render nothingcollapsed: inject for model, render a small marker (e.g. “additional context applied”)full: preserve current fully visible behaviorBut I’d treat this as a follow-up UX/config layer. The core issue is that
additionalContextcurrently flows through the same recording path as transcript-visible conversation items.mychmly commentedon May 5, 2026
Adding one more concrete large-context use case and validation point.
Environment:
codex-cli 0.128.0Darwin 25.4.0 arm64 armGhostty 1.3.1UserPromptSubmithooksUse case:
A project-level agent harness needs to inject a large session-start context block into the model. The payload is not ordinary chat content; it is source/context material such as:
The desired UX is:
Current behavior:
UserPromptSubmit.hookSpecificOutput.additionalContextis both model-visible and transcript-visible. In a real harness test, a new-session hook returned a compact report plus a source-pack-like context payload. Codex accepted the hook output, but the entire payload was rendered into the visible CLI transcript as hook/developer context.Observed result from a sanitized local test:
If the harness suppresses the large
additionalContextto avoid flooding the CLI, the model no longer receives the full session-start context. That creates a hard tradeoff:This is exactly the kind of case where a model-visible but hidden/collapsed hook context channel is needed.
Minimal reproduction shape:
Expected behavior:
Codex should provide a way for hooks to send context that is model-visible without rendering the entire payload into the visible transcript.
Potential API/implementation directions:
additionalContext, for example:Semantics:
hidden: model-visible, no transcript renderingcollapsed: model-visible, transcript shows only a small marker/summaryfull: current behavior, render the whole payload{ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "<large model-visible context>", "displayContext": "Hook applied: 200 KB model context injected" } }Record hook-provided
additionalContextinto the in-memory model history used for the next request, but do not persist/emit it through the same transcript/session-log/raw-response-item path as normal visible conversation items unless visibility is explicitlyfull.This would preserve the important behavior:
additionalContextfullThis issue is not about hiding tool execution from users. A compact marker or audit line is still useful. The problem is that the full model-context payload is currently forced into the human-facing transcript.
oxysoft commentedon May 8, 2026
Indexed this hook ticket in the umbrella tracker: #21753
Goal: collect the scattered Codex hook requests and bugs into one parity matrix for Full Claude Code Hook Parity (29+), while preserving this issue as the detailed thread for its specific behavior.
rsaulo commentedon May 18, 2026
I prepared a small patch for this in a fork because direct PR creation against
openai/codexis currently blocked for my account (CreatePullRequestpermission denied via GraphQL; REST returned 404).Patch/branch:
Approach:
additionalContextmodel-visible by recording it into in-memory conversation historyHookCompletedcontext entry text with a compact byte-count marker, so the full payload no longer floods the TUI/transcriptValidated locally with:
PATH="$HOME/.cargo/bin:$PATH" cargo test -p codex-core hook_runtime::testsPATH="$HOME/.cargo/bin:$PATH" cargo test -p codex-hooksThis is intended to address the core issue here while also aligning with #21696 / #20766.
Keesan12 commentedon May 18, 2026
The dangerous part is not only that �dditionalContext becomes visible, it is that the receipt boundary collapses. Hidden runtime context and visible transcript context serve different jobs, and once they merge, integrations can no longer rely on either privacy or clean postmortems. I would want Codex to keep a separate provenance tag for hook-provided hidden context so the model can consume it while the operator can still audit that it existed without seeing it rendered as a normal developer message.
hangaoke1 commentedon May 27, 2026
mark
Horacehxw commentedon May 31, 2026
I can reproduce this same class of issue with a Superpowers
SessionStarthook setup.Superpowers returns
hookSpecificOutput.additionalContextso the model receives startup instructions. Today Codex surfaces that context through two separate visible paths:HookCompletedentries asHookOutputEntryKind::Context, which the TUI renders ashook context: ...record_additional_contextscurrently usesrecord_conversation_items, making the context transcript-visibleI prepared a focused patch here:
The approach is intentionally narrower than a fold/collapsed UI feature:
additionalContextmodel-visiblesuppressOutput: trueforSessionStart,SubagentStart, andUserPromptSubmitcontext hook entriesPreToolUse/PostToolUsesuppressOutputbehavior unchanged for nowA direct PR against
openai/codexis currently blocked for my account withCreatePullRequestpermission denied, so I pushed the patch to a fork and linked the compare above.Maintainer questions:
additionalContextmodel-visible but transcript/raw-response-hidden?suppressOutputfirst forSessionStart/SubagentStart/UserPromptSubmit, while leaving the currently unsupported tool-hook cases alone?HookCompletedcontext entry stay fully visible unlesssuppressOutput: true, or would maintainers prefer hidden-by-default for alladditionalContext?zycaskevin commentedon May 31, 2026
Thanks for documenting this. I took a quick look and wanted to share a possible direction, in case it helps.
It looks like the core issue may be that hook
additionalContextis currently flowing through the same path that records conversation items and emits raw response items for the visible transcript.One possible fix direction could be to keep those hook-provided developer messages available to the model, but avoid emitting them through the raw response item path that the visible transcript consumes.
Concretely, that would mean:
additionalContextin the model-visible conversation historyI tried a small local patch in that direction and the focused hook runtime tests passed locally:
cargo fmt --checkcargo test -p codex-core hook_runtime::tests --libPatch reference, only as a discussion aid:
main...zycaskevin:fix/hook-additional-context-transcript-visibility
attila commentedon Jun 9, 2026
I hit the same blocker while porting lore's Claude Code integration to Codex.
Context: lore injects deterministic coding conventions through hook
hookSpecificOutput.additionalContext. On Claude Code this is model-visible but not displayed in theoperator transcript, which is essential: the injected payload can be full pattern files/pages of
project conventions.
I built a local Codex plugin/adapter and verified:
SessionStart,PreToolUse,UserPromptSubmit, andPostToolUsefixtures are acceptedadditionalContextreaches the modelBut the UX is not shippable because Codex renders the injected context visibly in the terminal. This means pages of hook-injected convention text are interleaved with normal agent output, making it hard for the operator to distinguish injected context from conversation.
For this integration, summarizing/truncating is not a viable workaround. The product requirement is full-fidelity, deterministic, model-visible context that is hidden from the operator transcript, as Claude Code provides.
Public branch with the experimental integration and blocker docs: https://github.com/attila/lore/tree/feat/codex-plugin
The requested capability is either:
hookSpecificOutput.additionalContextin the visible TUI transcript, while still making it model-visible; orWithout that, full-body hook context integrations are effectively blocked on Codex CLI.
fix(codex): ship a Codex-specific SessionStart hook with correct cont…
cowwoc commentedon Jun 17, 2026
Possible strong relation to #28736.
That bug reproduces
SessionStarthooks forsource=compactfiring on later turns when no new compaction actually took place, due to duplicate compact events being drained later. If a hook injectsadditionalContext, that can cause extra unexpected developer-message/context pollution on a later turn, which looks directly relevant to visible reinjection symptom here.markrogersjr commentedon Jul 6, 2026
I prepared a focused patch against current
mainat8268cbfb0e5f39cb4efff928264fe8f29ddacafb.The root cause is that
record_additional_contextscurrently callsrecord_conversation_items, which does three things at once: updates model history, persists the item to the rollout, and emitsRawResponseItem. Hook context needs only the first behavior.This patch:
additionalContextin the in-memory history sent to the model;suppressOutput: truefor theUserPromptSubmitcontext entry, while keeping stop/block/error feedback visible;suppressOutput;With the patch, a hook can request completely silent context injection with:
{ "suppressOutput": true, "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "model-visible context" } }Patch
Red/green validation:
suppress_output_hides_context_entryfailed because a fullContextentry was emitted.record_additional_contexts_stays_out_of_transcript_eventsfailed becauseevents.try_recv()received a raw response item.just test -p codex-hooks: 121 passed.just test -p codex-core hook_runtime::tests: 6 passed.just fmt: passed.git diff --check: passed.I did not run the complete workspace test suite; the results above are the focused affected-crate suites. I have not opened an upstream PR because
docs/contributing.mdsays external PRs are invitation-only. If a maintainer confirms this direction, I can package the same patch as an invited PR.cowwoc commentedon Jul 8, 2026
This seems to be fixed for me in v0.143.0. The output still renders to the user but using
… +1044 lines (ctrl + t to view transcript)to hide long output.attila commentedon Jul 13, 2026
For anyone tracking this: the two upstream attempts at a fix (the
UserInstructionsstack #30138–#30141 and the async hook stack #31885–#31887) were both closed unmerged in the week of 2026-07-06, and the community patch above is still awaiting review.I've asked for maintainer direction on the intended design over in #21696, since that's where the design options are laid out. This bug and that feature request resolve the same underlying coupling of model visibility and TUI rendering.
feat(codex): wire project-brain into SessionStart hook (#191)