Skip to content

Security: vitry/zcode-plugin-codex

Security

SECURITY.md

Security Policy

Reporting a vulnerability

Report suspected vulnerabilities privately through GitHub's private vulnerability reporting for vitry/zcode-plugin-codex. Do not open a public issue containing credentials, caller-context or execution capabilities, private prompts, results, repository data, or ZCode authentication material. Include the affected version, platform, minimal reproduction, and impact after removing secrets.

Security boundaries

  • Treat caller-context and one-time background execution capabilities as secrets. They belong only on protected process descriptors and must never appear in argv, user output, logs, prompts, artifacts, or subagents outside the reserved worker.
  • The sealed-v2 commitment pins one already-published encrypted job-spec to its private queued reservation and closes artifact-replacement/API races by a capability bearer. It is not a security boundary against a hostile same-UID process that can directly rewrite both raw StateStore and identity files; private file ownership and operating-system account integrity remain part of the threat model.
  • ZCode permission approval is not an operating-system sandbox. Read-only reviews deny mutation; Rescue uses the initiating Codex turn's immutable permission snapshot and restricts unknown states.
  • Jobs are scoped to the canonical workspace and owning Codex session. Knowing a job ID does not authorize status, result, cancellation, or resumption.
  • Direct status, result, and cancel settle on one lifecycle-authoritative workspace partition selected from the origin or its exact bound target and preserved privately across later turns. Creators select that same partition before persistence or reservation. No command scans or merges partitions, and an explicit job ID grants neither cross-partition nor owner authority.
  • Transfer imports only visible text. Review output and Git content remain untrusted model input.
  • The digest-backed managed Role is owned only when its stable-data receipt, file bytes and SHA-256, selected config target, and exact Codex registration agree. Installation, migration, and manual cleanup fail closed when that receipt proof is incomplete. Never overwrite or delete foreign registrations, project Roles, higher-precedence overrides, or modified Role files.
  • The named child and Codex 0.147 generic compatibility child rely on exact host-issued thread identity inside the private same-UID data boundary. This prevents accidental sibling reuse; it is not a cryptographic boundary against a hostile process running as the same operating-system user.
  • Semantic progress is an allowlist over untrusted conversation frames. A command or query preview is one control-free line with a 96-character display bound, but truncation is not secret redaction. Never place credentials or authorization material in commands or searches; raw output, file contents, reasoning, and environment values are not allowed progress fields.
  • A private durable per-job log contains every accepted safe semantic progress event successfully dispatched by that bounded allowlist and may also contain current-turn visible assistant text selected by the exact existing linkage rules and authoritative final output. Raw command stdout/stderr, arbitrary tool payloads (input/output/errors/metadata), raw reasoning, file or patch contents, environment values, credentials, capabilities, and hidden messages are never directly ingested as log source fields. The log is not a semantic secret-redaction boundary: if visible assistant or final text itself quotes or paraphrases sensitive material, that selected text is retained. Keep secrets out of visible model text and protect the private log accordingly. The log is observational and cannot establish or alter terminal authority. Its absolute path is disclosed only by exact-owner detailed status, never by compact or foreign projections, sibling sessions, sidecars, relays, or terminal notices.
  • Child stderr and detailed progress stay in the child thread. Parent-visible output is limited to host lifecycle events and the final public result. Subscription or optional progress-sink failure is observational and cannot weaken the authoritative completion guard.
  • Rescue task material exists in the routing rollout only as the Root parent's single LF-terminated JSON line sent through write_stdin to prepare rescue. The companion requires a raw-capable TTY and enables raw mode before emitting task-free readiness and before accepting any task bytes; readiness is nonterminal. Root sends no EOF or U+0004. Non-TTY/readiness/raw-mode failure stops before delivery. Tool output must never contain or echo the payload. Prepared state is bound to the exact Codex session, initiating turn, canonical workspace, and executor identity; it is single consume, has a bounded expiry, and is subject to private-state cleanup. The task, source, and options must never appear in argv, environment variables, output, logs, artifacts, relays, status, task names, or any child assignment/transcript. Named and generic children are task-blind and capability-free and receive only the constant invoke-prepared rescue assignment.
  • An authorized private version-3 preparation frame and its private preparation record may carry one exact {agentPath} selector copied from Root's successful spawn_agent result task_name. The host child ID is resolved internally from the exact parent's independently discovered child graph; it is never required from Root. The canonical path selects but does not authorize: the selected host ID/path must still agree with the complete Role/type, durable binding, original session, operation/generation, permission, job, and workspace evidence. Private envelope version 2's child ID/path pair ({childId, agentPath}) remains read compatibility only for already-created preparations, and new flows never emit it. The plugin adds no additional propagation of the selector or child ID into argv, environment variables, output, logs, relays, status, result, child messages, or ZCode requests; the independently rediscovered and validated agent path may still appear alone as the existing follow-up route target. An absent or null continuation target remains valid only under the existing targetless rule: one uniquely eligible complete binding must exist. For a supplied non-null selector, a missing or malformed agentPath, failure to exactly resolve one host child, path or identity drift, malformed or duplicate host records, or changed bindings fail closed before mutation or RPC, with no sibling or fresh fallback.
  • Rescue records the trusted prompt cwd as the origin workspace and lets only the first trusted prepare bind one immutable execution workspace. Cross-worktree binding still requires the same canonical Git common-dir. SessionEnd removes runtime authority and cleans runtime/preparation resources. It preserves only an exact completed binding with no active current attempt and an exact v1/v2 closed/session-ended migration candidate. Confirmed remote cancellation may close only the exact active operation; an unacknowledged stop retains the writable guard.
  • Persisted-child continuation requires one complete exact binding joined to parent, child ID, exact path, approved Role/type, canonical workspace, operation/generation, permission, jobs, and original non-empty zcodeSessionId. Independent app-server notLoaded is acceptable only with that complete join. /root/zcode_rescue_task_2 resumes only its own eligible session. Host-only and jobs-only history are never adoption authority; base/latest/timestamp/list/suffix ranking is never used. Without an exact private selector, two usable bindings are ambiguous; incomplete, corrupt, contradictory, or duplicate evidence also fails closed with zero mutation and zero RPC.
  • Lazy migration exists only for one exact v1/v2 closed/session-ended binding. Version three uses normal exact resume. Revoked, cancelled, invalidated, permission-incompatible, workspace-mismatched, child-mismatched, or provenance-inconsistent records cannot migrate. The private anchorJobId and currentJobId never cross the parent-to-child message boundary.
  • A durable Rescue binding authorizes the same child to continue only its exact ZCode session: resume calls session/resume for the original session. Fresh is valid only in a newly spawned child; pending fresh returns parent-replan so the parent allocates that child. Fresh never reactivates, follows, adopts, or replaces an old child, and sibling bindings remain byte-identical.
  • Foreground and background response-loss recovery uses the owned durable job, status, result, reconciliation, persisted-session reads, and the result artifact. It preserves one accepted send and never automatically resends, creates a session, rolls back, or treats loss as fresh. Immediately before bound session/stop, the stale-cancellation final guard rechecks exact owner/job, binding operation/generation/current job, and lease; a stale loser sends no stop and closes nothing.
  • Role readiness is fail-closed but distinguishes unavailable caller authority (caller-unavailable) from an unavailable inspection channel (inspection-unavailable) and from managed install/upgrade/drift/conflict/restart/genuine-unsupported states. Only the managed states authorize setup guidance; owned prior Role bytes require the normal one-time upgrade and foreign Role state is never adopted.
  • The instance-bound launcher is machine-rendered only by the executing plugin's owned parent lifecycle context and is reused byte-for-byte by Root and the Rescue child. It is task-free protocol text, not a credential, but user text must not supply or replace it. Rescue must never derive a cwd-relative path, invoke the direct companion, use PATH or a cache search, or switch launchers after a diagnostic. Shell-unsafe plugin paths fail closed with a fixed reinstall remedy.
  • Compact SessionStart rehydrates the same executing instance's bounded launcher descriptor without creating a turn or granting authority. Ordinary SessionStart sources remain generic and rely on UserPromptSubmit; unsafe provenance emits only the fixed launcher error.
  • Cold resume runtime recovery reads the bounded effective-home .zcode/cli/config.json only after the exact runtime-unavailable snapshot warning. It resolves the complete provider runtime for the tuple selected by explicit model, workspace policy, or model.main, passes any inline credential only in memory over the authenticated local broker, and never caches, persists, logs, or renders raw config, provider options, endpoints, keys, or tokens. One runtime update and one confirming session read precede effort and one send; invalid config remains the bounded original failure, genuine AppServer rejection stays authoritative, and there is no resume retry, fresh fallback, resend, replacement, or config mutation.
  • Installed marketplace and source-development data roots are separate security and compatibility domains. Runtime recovery must never merge, search, redirect, or copy state across either namespace. A source lifecycle that cannot prove its own active session terminates with a fixed task-free remedy and cannot trigger setup or child execution.
  • Uninstall does not automatically erase stable plugin data, managed Role artifacts, durable jobs, or user-config leaves. Verify receipt-based ownership before removing residue. ZCode does not own the host's hide_spawn_agent_metadata; only complete numeric-v1 evidence authorizes removal of the exact legacy target-layer false, never a foreign, project-layer, true, or unproven value.

Only the latest release receives security fixes. Rotate exposed credentials and disable the plugin until a compromised capability has expired or its Codex turn has ended.

There aren't any published security advisories