Codex encrypted its sub-agent prompts. Gate the spawn plan. Codex encrypted its sub-agent prompts, breaking the human-readable audit trail that developers used to review delegated tasks. A developer proposes a pre-dispatch authorization gate that checks a child agent's grants against policy before the handoff, arguing that control should happen before execution, not after via trace inspection. Pre-dispatch authorization for AI sub-agents means checking a child spawn's grant envelope its role, tools, path scope and token budget against the parent's policy in the last plaintext moment before handoff, not by reading a trace afterwards. When the orchestrator encrypts the handoff, the after-view goes blind. The before-view does not, because it sits earlier on the timeline. On July 14, "Codex starts encrypting sub-agent prompts" hit 408 points and 240 comments on Hacker News https://news.ycombinator.com/item?id=48905028 in a day. The tracking bug behind it is filed as openai/codex 28058 https://github.com/openai/codex/issues/28058 , titled "Regression: encrypted MultiAgentV2 messages remove readable task audit trail." A change encrypted the orchestrator-to-sub-agent payload, and the plaintext task record humans used to read after the fact turned into ciphertext. People who had been inspecting what their sub-agents were told, after dispatch, could no longer read it. That is a good thing to notice, and a worse thing to fix by asking for the plaintext back. AI disclosure.I wrote subagent dispatch gate.py with an AI assistant and ran it myself: Python 3.13.5, offline, standard library only, no network, no keys, no funds. Every number, exit code and sha256 below is pasted from a real local run. I ran the whole demo twice and the two output.txt files are byte-for-byte identical sha256 5af48191642d66f7c364c429c50d2ad1a021f09004f5566ba878c7be87fcaaf1 . The one synthetic part is clearly marked: encrypt artifact models theobservable consequenceof encryption opaque bytes you cannot parse back into fields , not Codex's real crypto. And every fact about Codex here comes from that HN thread and that issue. I did not reproduce their system. In short: fs.write , a fs: wildcard, shell.run , a path of ~ and a 5,000,000-token budget produces PASS: 1 BLOCK: 1 , PASS: 2 BLOCK: 0 , It is a check that runs on the parent, before the parent hands a task to a child agent. The parent holds a spawn plan: for each child, a role, a set of tool grants, a set of paths it may touch, and a token budget. Pre-dispatch authorization compares each of those against a policy the parent already owns, and refuses the spawn if any child asks for more than the policy allows. It happens while the plan is still a plain object in memory. Nothing has been sent, nothing has been encrypted, nothing has run. The distinction that matters: this is authorization of a spawn , not authorization of an action , and not reconciliation of a trace . It is the same family as a pre-execution gate for AI agents https://finops.spinov.online/blog/pre-execution-gate-for-ai-agents/ , moved up one level, from "may this action run" to "may this child exist with these powers." Read issue 28058 in its own words. A PR added encryption to the MultiAgentV2 message payloads. The intent was reasonable: the model-facing channel between orchestrator and sub-agent should not sit around in plaintext on disk. The side effect was that the local rollout history, the thing a human opened to review what task got delegated to which sub-agent , became unreadable. The issue's ask is to keep a human-readable audit copy alongside the encrypted delivery. I understand the ask. I also think it quietly gives up the game. If your control over what a sub-agent may do lives in your ability to read the transcript afterwards , then your control was always downstream of the work. The sub-agent already ran. The tokens already burned. The shell.run already happened. Restoring the readable copy lets you write a better postmortem. It does not stop the next spawn. This is the line I keep coming back to across this whole cluster: tracking is not control. A trace tells you what happened. It does not decide what is allowed. When the trace is plaintext you can pretend the two are the same, because you can always go read it and intervene. Encryption breaks the pretense. It takes away the after-view and forces the question: if you could not read the trace at all, where would your control actually live? Here is the honest version of that question, run as code. Same spawn, two worlds, one variable. python def encrypt artifact plan obj : """Model the at-rest / handoff artifact AFTER the orchestrator encrypts it. Not real crypto: models the observable consequence -- bytes a reader without the key cannot parse back into fields. sha256 stands in for 'ciphertext'.""" raw = json.dumps plan obj, sort keys=True .encode return "ENC:" + hashlib.sha256 raw .hexdigest def posthoc inspect artifact : """A session inspector reading the artifact AFTER dispatch. Plaintext JSON - recovers the plan worked yesterday . Opaque ENC blob - recovers nothing blind today .""" if isinstance artifact, str and artifact.startswith "ENC:" : return None, 0 blind: zero fields recoverable obj = json.loads artifact if isinstance artifact, str else artifact if isinstance obj, dict and isinstance obj.get "plan" , list : fields = sum len c for c in obj "plan" if isinstance c, dict return obj "plan" , fields return None, 0 The point of posthoc inspect is that it is not rigged. Hand it plaintext and it does its job. Run python3 subagent dispatch gate.py --contrast : CONTRAST: post-hoc inspection vs pre-dispatch gate same spawn world=PLAINTEXT post-hoc: recovered plan=yes fields=4 world=ENCRYPTED post-hoc: recovered plan=no fields=0 <- BLIND pre-dispatch reads parent's in-process plan: children=1 invariant to artifact encryption = post-hoc control SURVIVES encryption: no = pre-dispatch control survives encryption: yes sha256 contrast =0abca3747138eaf4260ed298ed001c1df6fd7ac67e89ce56f60c7b5fe8b9ef11 Yesterday's inspector recovered 4 fields. Today's recovers 0. It did not get worse at its job. The world moved out from under it. The pre-dispatch view reads the plan the parent constructs in memory, before encrypt artifact ever touches it, so it reports the same children=1 in both worlds. That is the whole argument in five lines of output: encryption is fatal to the after-view and irrelevant to the before-view, and I am not shaving points off post-hoc to get there. It honestly recovers the plan when the plan is readable. The reason the before-view can exist at all is boring and load-bearing: the parent has to build the plan in plaintext to send it. Even Codex's own encryption sits around delivery and at-rest storage. The payload is assembled in the clear first. There is a moment, every single spawn, when the full plan is a plain object the parent is holding. That moment is where the gate goes. Four checks, one policy, one child at a time. Nothing clever. WILDCARD PATH MARKERS = "~", "$HOME", " " WRITE CAPS = {"fs.write", "fs.delete", "shell.run", "wallet.transfer"} def escapes workspace path, root : """True if path is not provably inside root fail-closed on ambiguity .""" if not isinstance path, str : a non-string path spec is not provably safe return True p = path.strip root norm = root.rstrip "/" if p == "": return True for m in WILDCARD PATH MARKERS: home / wildcard touches everything if m in p: return True if ".." in p.split "/" : parent-traversal absolute OR relative return True if p == "/": filesystem root return True if p.startswith "/" : absolute: must be inside root return not p == root norm or p.startswith root norm + "/" return False def check child child, policy : reasons = allowed tools = set policy "allowed tools" cap = policy "budget cap tokens" root = policy "workspace root" allow write = policy.get "allow write", False is True only a real True grants write tools = child.get "tools" if not isinstance tools, list or not tools: reasons.append "child declares no tools fail-closed: unauthorizable " else: for t in tools: if t not in allowed tools: reasons.append "tool '%s' not in parent allowlist deny-by-default " % t if t in WRITE CAPS and not allow write: reasons.append "tool '%s' is a write/destructive capability but " "policy allow write=false" % t paths = child.get "paths" if not isinstance paths, list : reasons.append "child.paths must be a list" paths = for p in paths: if escapes workspace p, root : reasons.append "path '%s' escapes workspace root '%s'" % str p .strip , root budget = child.get "budget tokens" if not isinstance budget, int or isinstance budget, bool : reasons.append "budget tokens missing or non-integer unbounded spawn " elif budget cap: reasons.append "budget tokens %d over parent cap %d" % budget, cap elif budget <= 0: reasons.append "budget tokens %d must be positive" % budget return child.get "role", "