Guardian-approved unified exec drops canonical process identity and hides a live background wait

Open 💬 0 comments Opened Jul 19, 2026 by 92645417d9e5c763259dbebc306e3e

What version of Codex CLI is running?

codex-cli 0.144.23

The observed build is based on rust-v0.145.0-alpha.23 with local patches.

The locally modified source was compared with its upstream base before preparing this report. The local changes do not modify forced-rm classification, exec-policy prompt generation, Guardian routing conditions, the command_execution_started dedupe state, canonical unified-exec process identity, terminal-interaction correlation, or stale-poll filtering.

What subscription do you have?

ChatGPT Pro 20x

Which model were you using?

gpt-5.6-sol

What platform is your computer?

linux amd64/aarch64

What terminal emulator and version are you using, if applicable?

alacritty-0.16.1, tmux-3.4

What issue are you seeing?

When automatic Guardian review approves a unified-exec command that remains alive after the initial yield, the process continues running but the TUI can lose the command's canonical startup identity.

The Working bar then retains a reasoning-derived title such as:

Polling underlying system

The expected live-process title is:

Waiting for background terminal

The process itself is not necessarily hung or terminated. The failure is that app-server suppresses the canonical command start carrying the real process_id and UnifiedExecStartup source after a Guardian synthetic placeholder has already used the same item ID.

The TUI therefore cannot correlate later empty terminal polls with the live process. The valid poll looks unknown and is discarded by the stale-poll guard, leaving the reasoning-derived Working title visible.

This became substantially easier to observe after be39aab9f45fc76705590b063bc2480827571561 expanded forced-rm detection. Long build, test, and installation commands frequently contain a forced cleanup such as rm -rf "$temporary_directory", so many more complete tool calls now enter automatic review.

The expanded review frequency did not create the lifecycle defect. It increased the number of Guardian-approved long-running commands and exposed the already-latent synthetic-placeholder versus canonical-start collision.

What steps can reproduce the bug?

  • Enable automatic approval review with approvals_reviewer = "auto_review" and an approval policy that routes prompts through the reviewer.
  • Run a compound shell or unified-exec command that requires approval and remains alive after the initial tool yield.
  • A forced cleanup inside the compound command is one reliable way to produce the approval request on versions containing be39aab9f45fc76705590b063bc2480827571561.
  • Let Guardian approve the action.
  • Allow the real process to continue running in the background.
  • Poll the process with an empty write_stdin call.
  • Allow a reasoning summary title to arrive while the process is still live.
  • Observe that the process remains alive but the TUI does not show the correlated background wait and can retain the reasoning-derived title.

The minimal event sequence is:

GuardianAssessment(InProgress)
synthetic CommandExecution start
item/autoApprovalReview/started
item/autoApprovalReview/completed: approved
real unified-exec process starts
canonical CommandExecution start uses the same item ID
app-server suppresses the canonical start as a duplicate
empty TerminalInteraction poll carries the real process ID
TUI cannot correlate the process ID and ignores the poll
reasoning-derived Working title remains visible

The synthetic start has placeholder metadata:

id = target_item_id
source = Agent
process_id = None
status = InProgress

The later canonical start has the information required by the TUI:

id = target_item_id
source = UnifiedExecStartup
process_id = Some(real_process_id)
status = InProgress

App-server currently records only whether the item ID has started. Once the synthetic placeholder inserts the ID into turn_summary.command_execution_started, the later canonical start is treated as a complete duplicate rather than as a metadata upgrade.

The TUI calls track_unified_exec_process_begin only for UnifiedExecStartup. Seeing only the synthetic Agent item means it never establishes the real process mapping.

What is the expected behavior?

  • Guardian review ordering should remain intact, with the command item visible before the review starts.
  • The synthetic placeholder and canonical start should represent one logical command rather than two transcript items.
  • A canonical start following a synthetic placeholder should upgrade or upsert the same item with UnifiedExecStartup and the real process_id.
  • Empty polls from a live process should correlate with the command and show Waiting for background terminal.
  • Reasoning titles should not replace a valid live background-wait state.
  • Command completion should clear the process and wait state exactly once.
  • Guardian denial should complete the synthetic item without requiring a canonical process start.
  • Ordinary commands without Guardian review should preserve their existing lifecycle.
  • Empty polls from processes that have already exited should continue to be ignored.

Additional information

Existing Issue Search

The open and closed issue and pull-request search did not find an existing report for this exact Guardian synthetic-placeholder to canonical-start metadata loss.

Related reports such as #12033, #14303, #14314, and #22957 describe stale or repeated background waiting, generally after a command appears to have completed. This report is different because the process is still live and the canonical startup identity is lost before the empty poll reaches TUI correlation.

Relevant Upstream Commits

7532f34699fca93fc52cbb1ae77d153a70488382
  • Date: 2026-01-14.
  • Subject: fix: drop double waiting header in TUI (#9145).
  • Relevance: the TUI background-wait surface relies on unified-exec process correlation.
  • Ownership: presentation and wait-state handling, not Guardian approval frequency.
bd30bad96f92de4d6b7f2429ddfc322937bc670a
  • Date: 2026-04-06.
  • Subject: fix(guardian): fix ordering of guardian events (#16462).
  • Relevance: creates the necessary pre-review synthetic command item with placeholder source and process metadata.
  • Ownership: first half of the lifecycle collision.
e43a2e297fe72285890e0f04f9f1aece843c2338
  • Date: 2026-05-19.
  • Subject: Fix stale background terminal poll events (#23231).
  • Relevance: correctly ignores empty polls for unknown or exited processes.
  • Interaction: a live process also appears unknown when its canonical startup metadata was suppressed.
aa94ea139744f0a9dd76343805cc96649d9f2658
  • Date: 2026-07-06.
  • Subject: chore(approvals) consolidate guardian calls for shell tools (#31267).
  • Relevance: centralizes routing of an already-created shell approval request through the neutral approval action.
  • Ownership: Guardian routing after approval generation, not forced-rm classification.
cca16a10878202cb2f6e9666b6b4330329ea7e65
  • Date: 2026-07-06.
  • Subject: feat(core): emit canonical command execution items (#31297).
  • Relevance: emits the canonical command start with the real unified-exec source and process identity.
  • Ownership: second half of the lifecycle collision when the same item ID was already occupied by the Guardian placeholder.
be39aab9f45fc76705590b063bc2480827571561
  • Date: 2026-07-16.
  • Subject: Strengthen forced rm command detection (#33464).
  • Relevance: expanded deterministic forced-rm recognition across control flow, substitutions, pipelines, traps, nested shells, sudo, env, executable paths, and force-option placement.
  • Version boundary: absent from rust-v0.145.0-alpha.13 and present in rust-v0.145.0-alpha.20, rust-v0.145.0-alpha.22, and rust-v0.145.0-alpha.23.
  • Discovery relationship: increased Guardian Review frequency and made the lifecycle collision easy to reproduce, but did not create the item lifecycle defect.

Trigger Classification Detail

The current deterministic rule is not a raw substring check for rm.

It recognizes a literal rm invocation carrying -f or --force, including combined, separated, reordered, wrapped, or nested forms.

Examples now requiring approval include:

/bin/rm -fr target
rm -r -f target
rm --force target
rm target -f
sudo rm -rf target
env TARGET=target rm -rf target
if test -d target; then rm -rf target; fi
echo "$(rm -rf target)"
trap 'rm -rf target' EXIT

The rule does not classify these as forced rm:

rm -r target
echo 'rm -rf target'
cmd=rm; $cmd -rf target

A plain rm target can still be reviewed if the main model explicitly requests require_escalated or another policy rule requires approval, but that is separate from the deterministic forced-rm expansion.

Suggested Fix Invariant

No prior start: emit the canonical start.
Prior canonical start: suppress the true duplicate.
Prior synthetic placeholder: update or upsert source and process_id without duplicating the logical command.

The synthetic start should not be removed because it preserves Guardian review ordering.

The app-server started state should distinguish a synthetic placeholder from a canonical command rather than represent both with one boolean item-ID set.

Suggested Regression Coverage

  • Guardian approved plus long-running unified exec plus empty live poll.
  • Guardian approved plus short-lived command completion.
  • Guardian denied without a real process start.
  • Non-Guardian and manually approved command lifecycles.
  • Stale empty poll after process exit.
  • Reasoning deltas while a valid background wait is active.
  • Two concurrent Guardian-approved commands with distinct process IDs.
  • One logical command transcript item across placeholder-to-canonical upgrade.

Local Modification Check

The issue was reproduced on a locally modified Codex CLI build.

The local modifications were checked before this report was prepared and do not touch the relevant forced-rm classifier, exec-policy approval generation, Guardian routing condition, synthetic-start dedupe state, canonical process metadata, terminal-interaction correlation, or stale-poll guard.

No private patch commit names or unpublished implementation details are included in this report.

Authorship Note

This issue report and its source-history analysis were prepared with assistance from Codex CLI.

View original on GitHub ↗