Tracking issue for an in-progress fix (not yet merged). The change lives on branch
don/agents/pasted-text-processing and touches only
src/vs/platform/agentHost/node/codex/codexAgent.ts. Filed to track the remaining work before it is
safe to ship.
TLDR — what was broken (as a user)
In the Agents window with the Codex harness, project hooks silently don't run. A hook is a
small automation you set up in a project (e.g. "when a session starts, run this command", configured in
.codex/hooks.json). You'd configure one, start a Codex session… and nothing happened — no hook, no
error, no explanation. It just looked broken.
Why: Codex has a safety rule — because a hook runs real shell commands on your machine (outside the
model's tool-approval/sandbox flow), Codex refuses to run a project's hook until you've explicitly
trusted that exact hook (by content hash), so a malicious repo can't auto-run code. The VS Code
Agents window has no button or prompt to grant that trust, so the hook was always skipped.
(Everything else in Codex — skills, instructions/AGENTS.md, MCP servers, custom agents, file edits —
works. This gap is specific to hooks.)
TLDR — how we can fix it
When VS Code starts a Codex session, hand Codex the hook's trust for that session only — so the
hook runs without leaving VS Code and without editing the user's global ~/.codex/config.toml and
without a global bypass.
Concretely (implemented in codexAgent.ts): before each thread/start / thread/resume /
thread/fork, call the codex app-server hooks/list, then inject into the thread config:
config['hooks.state'][<hook.key>] = { trusted_hash: <hook.currentHash> }
- New helper
_buildSessionHookTrustState(client, cwds) — reads hooks/list, returns
{ [hook.key]: { trusted_hash: currentHash } } for each non-managed hook.
- New helper
_applyHookTrustState(config, trust) — merges it into the thread config.
- Wired into all four start paths (ordinary
thread/start, direct-backing thread/start,
thread/fork, thread/resume).
Intended safety gate: rely on existing VS Code Workspace Trust — a hook only runs at
turn/start, and the workbench refuses to process a request until the working directory is
workspace-trusted (agentHostSessionHandler._ensureWorkspaceTrust; secondary folders gated by
AgentHostSessionWorkingDirectorySynchronizer). Trust is scoped to the thread; a wrong/absent hash
still leaves the hook untrusted. ⚠️ This gate turned out to be incomplete — see the Security review
section.
Verified (codex 0.146.0): with the user's ~/.codex/config.toml stripped of all [hooks.state]
(so only the code can trust) and no bypass flag, the Codex SessionStart hook fired in the Agents
window (2 sessions → 2 markers); ~/.codex/config.toml was not modified; no-hook/normal sessions
still worked; typecheck-client passed.
Where it matches OAI's screenshot
The approach came from an OpenAI suggestion (via Slack):
"when starting a thread, include both the host-provided hook and its matching trust hash. that
should let codex trust only that specific hook for the session, without bypassing trust checks
globally or modifying the user's config. the catch is that the hook key/hash format isn't currently a
stable public interface."
| OAI's phrasing |
What we did |
Match? |
"when starting a thread" (at thread/start) |
injected at thread/start.config |
✅ |
| "its matching trust hash" |
hooks.state.<key>.trusted_hash = currentHash |
✅ |
| "trust only that specific hook for the session" |
session-scoped; absent/wrong hash → skipped |
✅ |
| "without … modifying the user's config / global bypass" |
~/.codex/config.toml untouched; no bypass flag |
✅ |
| "hook key/hash format isn't a stable public interface" |
hit exactly (the session_start vs sessionstart key bug) |
✅ |
Where it does NOT match (and the errors/issues hit getting here)
-
"include … the host-provided hook" — not done. OAI says the host provides both the hook and
its hash — most safely a host-authored / vendored hook (trusted provenance). Our implementation
supplies only the hash for a workspace on-disk hook (.codex/hooks.json) that codex reads off
disk. Reflecting a workspace hook's own hash back as trust is the security-relevant shortcut (see
below); "host-provided" is ambiguous (host-authored vs host-relayed) and that ambiguity is
the safe-vs-risky fork. Supplying a genuine host-provided hook definition via thread/start.config
(the HooksToml shape is { events, state }) was not validated.
-
The "hook key/hash format isn't a stable public interface" catch bit us. The trust key must be
HookMetadata.key verbatim (e.g. …/.codex/hooks.json:session_start:0:0). An early attempt
reconstructed it by lowercasing the event name to sessionstart (no underscore) → trust silently
failed. hooks/list already returns both key and currentHash, and currentHash is the value
to use as trusted_hash (confirmed for 0.146.0).
-
A CLI reproduction failed and produced a wrong intermediate conclusion. Supplying the same
hooks.state trust via codex exec -c did not trust the hook (even with a parse-confirmed inline
table), and neither did a project-level .codex/config.toml layer. An independent reviewer inferred
from codex-rs source that -c and thread/start.config "fold into the same SessionFlags layer", so
we first wrote the workaround up as unverified/infeasible. That was wrong: driving the
app-server directly (initialize → hooks/list → thread/start{config.hooks.state} → turn/start) and
then patching the real agent host both fired the hook — empirically, the thread/start.config
request path does honor hook trust while CLI -c did not. Lesson: the decisive path is
thread/start.config with the exact hook.key.
Security review — all issues and details
Reviewed by two independent specialists (GPT‑5.6 Sol, max/1M and Opus 4.8 Fast, max/1M). They
converged: the "hooks only run at turn/start, which is Workspace-Trust-gated" invariant is
incomplete — several paths let a hook execute without that gate.
| # |
Severity |
File |
Lines |
Vulnerability |
Confidence |
| 1 |
🟠 HIGH |
src/vs/platform/agentHost/node/codex/codexAgent.ts |
6088 (helper); applied 5349 + archive path |
Helper trusts every hook event, not just SessionStart. SessionEnd hooks run at thread shutdown/archive — no turn, no Workspace-Trust check. Opening + archiving a session for an untrusted folder executes its hook. Breaks the "only fires at turn/start" safety basis. |
10/10 |
| 2 |
🟠 HIGH |
src/vs/platform/agentHost/node/codex/codexAgent.ts (+ sessionServerTools.ts) |
3860, 4153, 4395, 5349 |
Second turn-dispatch path via the create_session/send_message server tools runs entirely in the node process, which has no workspace-trust concept. create_session accepts an arbitrary absolute workspace path → hooks in an attacker-planted .codex/hooks.json execute (outside codex sandbox/approval) with no trust prompt. |
8/10 |
| 3 |
🟠 HIGH |
src/vs/platform/agentHost/node/codex/codexAgent.ts |
4393-4408, 5348-5360 |
cwd mismatch: the workbench _ensureWorkspaceTrust can validate the default workspace while a restored/resumed session actually runs in a persisted session.workingDirectory; hooks from that never-validated folder get trusted and run on the next turn. |
9/10 |
| 4 |
🟡 MEDIUM |
src/vs/platform/agentHost/node/codex/codexAgent.ts |
6088 |
Filter is !isManaged && currentHash — ignores trustStatus, so it reflects the hash for modified hooks too, disabling codex's re-review-on-change. A changed .codex/hooks.json (git pull / compromised dep / collaborator) in an already-trusted folder auto-runs on the next turn with no re-prompt. |
6/10 |
Detail
#1 — SessionEnd/non-SessionStart hooks (Sol, 10/10). The helper reflects trust for all hooks
it finds, and SessionEnd executes at thread shutdown/thread/archive, which is dispatched directly
(no turn, no _ensureWorkspaceTrust). Merely opening a restored session in an untrusted folder and
archiving it can run its shell hook. Directly falsifies the linchpin assumption in the code comment.
Suggested: restrict reflected trust to sessionStart hooks, or require authoritative trust immediately
before every hook lifecycle event including archive/shutdown.
#2 — Un-gated server-tool path (Opus 7/10; corroborated by Sol → 8/10 consolidated).
create_session/send_message are contributed to every agent-host session and dispatch a turn via
agentService._startSessionPrompt inside the node process, which has no IWorkspaceTrust* anywhere.
Full chain verified: startPrompt → _startSessionPrompt → ChatTurnStarted → _sendMessage → _materializeIfNeeded (thread/start applies hook trust) → turn/start → codex run_turn → queued SessionStart hook executes. create_session({ workspace: "/attacker/path", … }) takes an arbitrary
path. Escalation over normal sub-agents: hooks bypass the sandbox/approval the sub-agent's own tools
obey. Suggested: constrain server-tool working directories to already-trusted roots, or gate hook-trust
reflection on an explicit per-directory trust signal from the workbench.
#3 — cwd/trust mismatch on restored sessions (Sol, 9/10). For an existing session in a regular
editor window, the trust resolver can return undefined → _ensureWorkspaceTrust validates the
current/default workspace, while the session runs in its persisted session.workingDirectory. The
change trusts hooks from that persisted (unvalidated) folder. Suggested: enforce trust at the agent-host
turn-dispatch boundary using the authoritative working-directory set, for both client- and
server-initiated turns, before applying hook trust.
#4 — Re-review-on-change disabled (Opus, residual/MEDIUM). !hook.isManaged && hook.currentHash
ignores trustStatus, so it re-trusts modified hooks. Consistent with VS Code's folder-scoped trust
model (which also doesn't re-prompt on content change), but a defense-in-depth reduction vs. codex's
design; closable by only reflecting the hash when trustStatus === "trusted".
What both reviewers confirmed is sound
- Prewarm/direct-backing
thread/start only queues SessionStart (verified against upstream codex:
executed later in run_turn), so eager thread creation alone doesn't run hooks.
- Fail-closed error handling (
hooks/list failure → {} → no trust).
- No string-injection surface (
hook.key/hash go into a structured JSON object, sourced from codex, not
raw workspace bytes).
!isManaged can't be abused for broader trust (a repo can't mark itself managed).
Bottom line
The core mechanism is right, but the Workspace-Trust gate I relied on does not cover every
hook-execution path — the fix is not safe to ship as-is. The through-line fix both reviewers point
to: enforce trust at the agent-host turn/lifecycle boundary using the session's authoritative working
directory (covering server-initiated turns, resume, and archive/SessionEnd), rather than leaning on
the renderer's _ensureWorkspaceTrust.
TLDR — what was broken (as a user)
In the Agents window with the Codex harness, project hooks silently don't run. A hook is a
small automation you set up in a project (e.g. "when a session starts, run this command", configured in
.codex/hooks.json). You'd configure one, start a Codex session… and nothing happened — no hook, noerror, no explanation. It just looked broken.
Why: Codex has a safety rule — because a hook runs real shell commands on your machine (outside the
model's tool-approval/sandbox flow), Codex refuses to run a project's hook until you've explicitly
trusted that exact hook (by content hash), so a malicious repo can't auto-run code. The VS Code
Agents window has no button or prompt to grant that trust, so the hook was always skipped.
(Everything else in Codex — skills, instructions/
AGENTS.md, MCP servers, custom agents, file edits —works. This gap is specific to hooks.)
TLDR — how we can fix it
When VS Code starts a Codex session, hand Codex the hook's trust for that session only — so the
hook runs without leaving VS Code and without editing the user's global
~/.codex/config.tomlandwithout a global bypass.
Concretely (implemented in
codexAgent.ts): before eachthread/start/thread/resume/thread/fork, call the codex app-serverhooks/list, then inject into the thread config:_buildSessionHookTrustState(client, cwds)— readshooks/list, returns{ [hook.key]: { trusted_hash: currentHash } }for each non-managed hook._applyHookTrustState(config, trust)— merges it into the thread config.thread/start, direct-backingthread/start,thread/fork,thread/resume).Intended safety gate: rely on existing VS Code Workspace Trust — a hook only runs at
⚠️ This gate turned out to be incomplete — see the Security review
turn/start, and the workbench refuses to process a request until the working directory isworkspace-trusted (
agentHostSessionHandler._ensureWorkspaceTrust; secondary folders gated byAgentHostSessionWorkingDirectorySynchronizer). Trust is scoped to the thread; a wrong/absent hashstill leaves the hook untrusted.
section.
Verified (codex 0.146.0): with the user's
~/.codex/config.tomlstripped of all[hooks.state](so only the code can trust) and no bypass flag, the Codex
SessionStarthook fired in the Agentswindow (2 sessions → 2 markers);
~/.codex/config.tomlwas not modified; no-hook/normal sessionsstill worked;
typecheck-clientpassed.Where it matches OAI's screenshot
The approach came from an OpenAI suggestion (via Slack):
thread/start)thread/start.confighooks.state.<key>.trusted_hash = currentHash~/.codex/config.tomluntouched; no bypass flagsession_startvssessionstartkey bug)Where it does NOT match (and the errors/issues hit getting here)
"include … the host-provided hook" — not done. OAI says the host provides both the hook and
its hash — most safely a host-authored / vendored hook (trusted provenance). Our implementation
supplies only the hash for a workspace on-disk hook (
.codex/hooks.json) that codex reads offdisk. Reflecting a workspace hook's own hash back as trust is the security-relevant shortcut (see
below); "host-provided" is ambiguous (host-authored vs host-relayed) and that ambiguity is
the safe-vs-risky fork. Supplying a genuine host-provided hook definition via
thread/start.config(the
HooksTomlshape is{ events, state }) was not validated.The "hook key/hash format isn't a stable public interface" catch bit us. The trust key must be
HookMetadata.keyverbatim (e.g.…/.codex/hooks.json:session_start:0:0). An early attemptreconstructed it by lowercasing the event name to
sessionstart(no underscore) → trust silentlyfailed.
hooks/listalready returns bothkeyandcurrentHash, andcurrentHashis the valueto use as
trusted_hash(confirmed for 0.146.0).A CLI reproduction failed and produced a wrong intermediate conclusion. Supplying the same
hooks.statetrust viacodex exec -cdid not trust the hook (even with a parse-confirmed inlinetable), and neither did a project-level
.codex/config.tomllayer. An independent reviewer inferredfrom codex-rs source that
-candthread/start.config"fold into the sameSessionFlagslayer", sowe first wrote the workaround up as unverified/infeasible. That was wrong: driving the
app-server directly (
initialize → hooks/list → thread/start{config.hooks.state} → turn/start) andthen patching the real agent host both fired the hook — empirically, the
thread/start.configrequest path does honor hook trust while CLI
-cdid not. Lesson: the decisive path isthread/start.configwith the exacthook.key.Security review — all issues and details
Reviewed by two independent specialists (GPT‑5.6 Sol, max/1M and Opus 4.8 Fast, max/1M). They
converged: the "hooks only run at
turn/start, which is Workspace-Trust-gated" invariant isincomplete — several paths let a hook execute without that gate.
SessionStart.SessionEndhooks run at thread shutdown/archive — no turn, no Workspace-Trust check. Opening + archiving a session for an untrusted folder executes its hook. Breaks the "only fires at turn/start" safety basis.create_session/send_messageserver tools runs entirely in the node process, which has no workspace-trust concept.create_sessionaccepts an arbitrary absoluteworkspacepath → hooks in an attacker-planted.codex/hooks.jsonexecute (outside codex sandbox/approval) with no trust prompt._ensureWorkspaceTrustcan validate the default workspace while a restored/resumed session actually runs in a persistedsession.workingDirectory; hooks from that never-validated folder get trusted and run on the next turn.!isManaged && currentHash— ignorestrustStatus, so it reflects the hash formodifiedhooks too, disabling codex's re-review-on-change. A changed.codex/hooks.json(git pull / compromised dep / collaborator) in an already-trusted folder auto-runs on the next turn with no re-prompt.Detail
#1 —
SessionEnd/non-SessionStarthooks (Sol, 10/10). The helper reflects trust for all hooksit finds, and
SessionEndexecutes at thread shutdown/thread/archive, which is dispatched directly(no turn, no
_ensureWorkspaceTrust). Merely opening a restored session in an untrusted folder andarchiving it can run its shell hook. Directly falsifies the linchpin assumption in the code comment.
Suggested: restrict reflected trust to
sessionStarthooks, or require authoritative trust immediatelybefore every hook lifecycle event including archive/shutdown.
#2 — Un-gated server-tool path (Opus 7/10; corroborated by Sol → 8/10 consolidated).
create_session/send_messageare contributed to every agent-host session and dispatch a turn viaagentService._startSessionPromptinside the node process, which has noIWorkspaceTrust*anywhere.Full chain verified:
startPrompt → _startSessionPrompt → ChatTurnStarted → _sendMessage → _materializeIfNeeded (thread/start applies hook trust) → turn/start → codex run_turn → queued SessionStart hook executes.create_session({ workspace: "/attacker/path", … })takes an arbitrarypath. Escalation over normal sub-agents: hooks bypass the sandbox/approval the sub-agent's own tools
obey. Suggested: constrain server-tool working directories to already-trusted roots, or gate hook-trust
reflection on an explicit per-directory trust signal from the workbench.
#3 — cwd/trust mismatch on restored sessions (Sol, 9/10). For an existing session in a regular
editor window, the trust resolver can return
undefined→_ensureWorkspaceTrustvalidates thecurrent/default workspace, while the session runs in its persisted
session.workingDirectory. Thechange trusts hooks from that persisted (unvalidated) folder. Suggested: enforce trust at the agent-host
turn-dispatch boundary using the authoritative working-directory set, for both client- and
server-initiated turns, before applying hook trust.
#4 — Re-review-on-change disabled (Opus, residual/MEDIUM).
!hook.isManaged && hook.currentHashignores
trustStatus, so it re-trustsmodifiedhooks. Consistent with VS Code's folder-scoped trustmodel (which also doesn't re-prompt on content change), but a defense-in-depth reduction vs. codex's
design; closable by only reflecting the hash when
trustStatus === "trusted".What both reviewers confirmed is sound
thread/startonly queuesSessionStart(verified against upstream codex:executed later in
run_turn), so eager thread creation alone doesn't run hooks.hooks/listfailure →{}→ no trust).hook.key/hash go into a structured JSON object, sourced from codex, notraw workspace bytes).
!isManagedcan't be abused for broader trust (a repo can't mark itself managed).Bottom line
The core mechanism is right, but the Workspace-Trust gate I relied on does not cover every
hook-execution path — the fix is not safe to ship as-is. The through-line fix both reviewers point
to: enforce trust at the agent-host turn/lifecycle boundary using the session's authoritative working
directory (covering server-initiated turns, resume, and archive/
SessionEnd), rather than leaning onthe renderer's
_ensureWorkspaceTrust.