Skip to content

Security and privacy

The bridge adds a local process boundary; it does not create an account, grant a provider permission, or replace either host's authentication and approval policy. Its setup lifecycle can apply verified native configuration or repair operations only after an exact plan approval. Install and use it only in a workspace where the selected peer command-line interface is trusted to receive the requested task and project context.

Authority boundaries

  • The caller chooses the verb, task, model, effort, and soft timeout within the published input contract.
  • The host supplies the workspace for Codex-to-Claude Code MCP calls. The reverse MCP tools reject model-supplied workspace, background, and session_id values.
  • Git membership is not a trust boundary: --skip-git-repo-check permits an explicitly selected non-Git workspace but does not choose or widen it. The caller and host must trust the selected directory.
  • implement may change the selected worktree under the peer host's normal permission mode; advise and review are read-only bridge operations.
  • Setup configuration and repair may change selected host/plugin configuration only through an expiring, one-use operation approved for the exact action, observed target, scope, workspace, state, argv identity, external capability, rollback record, and stop condition. A host-held per-user HMAC key prevents caller-side approval tampering; it does not grant consent, which remains owned by the operator or host permission surface. Authentication and live verification require distinct action-bound approvals that cannot be substituted for configuration. The bridge never grants an approval, bypasses an authentication prompt, reads credential files, or invents a model account.
  • Current-host plugin installation, trust, authentication, and fresh-session activation are external bootstrap boundaries. Setup cannot repair the invocation surface from which it was loaded.
  • bridge_status is a zero-provider, read-only MCP control-plane tool. It accepts no workspace override and returns sanitized identity, version, negotiated protocol, status schema 2.0, binding status/identity, confirmed canonical workspace, workspace fingerprint, and expected tool inventory. Unbound workspace and fingerprint are null; unbound is not evidence of a stale installation.
  • A recursion depth of one or greater is refused before another peer process is launched.

The host's normal permission controls remain authoritative. Review the workspace and permission mode before allowing a write-capable implementation, especially when a detached job may continue after the calling turn ends.

Sandbox: egress, environment, and quotas

The bridge selects the child's sandbox per verb rather than inheriting a default, and states each choice explicitly so the posture does not change when a peer CLI changes its own defaults.

  • Sandbox mode. advise and review run the Codex child under -s read-only; implement runs under -s workspace-write; a caller opt-in (--network github-read, below) replaces -s with a permission profile. --ignore-user-config prevents a user configuration file from widening either. The Claude child receives --permission-mode plan for read verbs and acceptEdits for implement, and read verbs additionally pass --disallowed-tools Edit Write.
  • Commits. An implement prompt tells the peer, in both directions, to leave every change unstaged and uncommitted and to run no Git write, git push included; the caller reviews the diff and commits. This is a prompt instruction, not a sandbox control: a Codex peer runs workspace-write with approval_policy="never" and still loads project instructions, so a local Git approval grant for that repository would otherwise make a completion commit its default. A bridge child gains nothing from any approval grant or push token in the checkout: each is the authority record of the calling session that owns the task, and no bridge step assigns a commit to the child.
  • Egress. Unless the caller opts in (next bullet), sandbox_workspace_write.network_access=false is passed explicitly on every Codex invocation. Network access is the cheapest of the three lethal-trifecta capabilities to remove: the child still reads the workspace and still writes files, but cannot send their contents anywhere. Pinning it means the bridge's egress posture is a property of the bridge, not of the installed Codex version's default. This control is Codex-side only and has no equivalent for the Claude child, whose tool use is constrained by permission mode rather than by a network switch. It also changes what a Codex implement can do: a task that installs a dependency, refreshes a lockfile, or runs a network-touching pre-commit hook now fails inside the sandbox where it previously succeeded.
  • Egress opt-in. --network github-read is the only widening, chosen per call by the caller and never by task text. It replaces the pin and -s with a permission profile that extends :read-only for advise/review and :workspace for implement, named at random per call (bridge-github-<random>), so no configuration layer, such as a trusted project's .codex/config.toml, can predefine a profile of the same name and merge extra domains into it (a probe showed that merge when the name was predictable). The profile states every key itself, because --ignore-user-config means none is inherited: network.domains allows api.github.com only; network.mode="limited" restricts requests to GET, HEAD and OPTIONS (without it an authenticated POST reaches GitHub, which a probe showed); .git, .codex and .agents at the workspace root are read-only; nested copies stay writable, as under the legacy sandbox (a profile that extends :workspace otherwise loses the built-in protection, and a write there would run code outside the sandbox on the next git command or widen later Codex sessions); and the network_proxy feature is on, because codex exec enforces the allowlist only with it (a probe without it let example.com through). network_proxy is experimental in Codex 0.162.1, so the opt-in can break with a Codex update; an unknown feature or an invalid value stops the call and a missing domain list blocks everything, but an unknown profile key is silently ignored, so a Codex release that renames network.mode or reshapes filesystem would drop that restriction without an error. The diagnose baseline only checks that --enable exists and cannot detect this, so every opted-in call first runs a preflight: the same overrides under codex sandbox, in a throwaway workspace with an empty CODEX_HOME (the child runs with --ignore-user-config, so the user's config layer must not shape the probe either), must run a trivial command (and, for implement, write a workspace file) so the later probes cannot pass merely because the sandbox never started, deny a .git write, get 200 from a GET to api.github.com/rate_limit (an endpoint that does not spend the unauthenticated quota), get the proxy's own 403 with x-proxy-error: blocked-by-method-policy for a POST (GitHub's own 403 does not count), and have example.com refused by the proxy or unreachable. Any probe that does not hold refuses the call (refused, fault network-preflight) before the Codex child starts. The preflight runs after effort validation and a cancel check; its time counts against the request's time budget, and the Codex child, and any retry of it, gets what remains of it in whole seconds; less than one second left refuses the call, and so does a request whose prompt, rewritten with the reduced budget, no longer fits the transport limit. On the probing host it adds about 1.5 to 3 seconds, depending on the verb, and three requests per call. Removing the method limit, the filesystem keys or the domain list from the overrides, or adding an invalid value, made the preflight refuse with the matching probe named. Not mirrored: the project config layer (the preflight cannot run in the caller's workspace; the random profile name keeps that layer from extending the profile) and managed requirements (codex sandbox loads them only with an explicit --permission-profile, while the bridge selects its profile through default_permissions; on a host with managed requirements, whether they change the run's profile is unverified). Probes under codex sandbox on macOS showed: GET api.github.com 200 and a gh code-scanning read succeeding, POST 403, example.com blocked, workspace writes allowed for implement and denied for the read verbs, .git and .codex writes denied. The opt-in is refused outside macOS, the only host where enforcement was probed; fetch the data in the caller there. Not verified: a resumed session re-applying a posture saved in its rollout. Residual risk: a GET to api.github.com can still carry workspace content in its query string or path, and gh authenticates with the user's own credentials; the prompt asks the peer to send read requests only and no workspace content, which is an instruction, not a control. gh api graphql uses POST and is blocked. Prefer fetching the data yourself and passing it in the task file; the opt-in is for a lookup the caller chose. Codex-side only: the Claude peer has no equivalent and the request is refused. The chosen posture is recorded in each incident file and each health record, not in the public envelope. The child environment is an allowlist, so GH_TOKEN, GITHUB_TOKEN and GH_CONFIG_DIR are not forwarded: gh inside the sandbox works with keyring login, and a user who authenticates through an environment token must name it in BRIDGE_CHILD_ENV_EXTRA (which hands that credential to the child) or the lookup fails unauthenticated.
  • Environment. The child environment is built from an allowlist, never inherited wholesale. Base process keys, provider authentication keys, and — on Windows only — the interpreter start-up keys survive; every other variable, including unrelated API keys and cloud credentials present in the calling session, is dropped. A variable that is filtered out is omitted rather than blanked, so the child cannot distinguish it from unset. Dropping the session's inter-agent messaging variables is part of the point: a peer process no longer inherits the ability to address other sessions.
  • Environment escape hatch. The fixed sets assume a direct-API-key login on a host with open egress. A host that authenticates through a proxy or an enterprise provider path needs variables no fixed list can anticipate, and would otherwise fail with an opaque provider error inside the child. Name them in BRIDGE_CHILD_ENV_EXTRA, comma-separated, and they are added to the allowlist for that host — for example BRIDGE_CHILD_ENV_EXTRA="HTTPS_PROXY,NO_PROXY,SSL_CERT_FILE" behind a proxy, or BRIDGE_CHILD_ENV_EXTRA="CLAUDE_CODE_USE_BEDROCK,AWS_PROFILE,AWS_REGION" on Bedrock. Only the calling process can set it; nothing a request carries reaches it, and the marker variable is never forwarded to the child.
  • Quotas. Each attempt carries a soft budget in the prompt and a hard cutoff at timeout_seconds * 1.2, after which the peer process tree is terminated. Pending stdout records are bounded independently, and retained stdout plus stderr reserve transcript-wrapper space within 256 KiB. Only complete valid Codex command_execution events omit their aggregated_output payload and record aggregated_output_bytes plus aggregated_output_sha256 over the decoded output re-encoded as UTF-8; an oversized record or retained-output overflow during capture or final flush produces an output-limit incident, including when timeout or cancellation also occurs. The timeout and output limits produce structured envelopes rather than silent kills.

Recursion is capped independently: a depth of one or greater is refused before another peer process starts, and the inherited depth marker cannot be lowered by a caller.

Data flow and persistence

The task text and selected project context are passed to the provider command-line interface named by the request direction. Provider account access, billing, retention, and model availability remain governed by that provider and its host configuration. The bridge does not upload artifacts to a separate service and does not persist credentials. Setup's static planning and configuration path does not make a provider call; authentication and live inference are distinct, separately approved capabilities. The host skill never launches authentication through a model-captured shell: it hands the approved setup command to the operator's own terminal, where Bridge inherits the provider flow without receiving its output.

Each child attempt writes one bounded transcript under .temp/bridge/. Detached jobs write metadata under .temp/bridge/jobs/; faults write sanitized records under .temp/bridge/incidents/; completed calls append one health record to .temp/bridge/health.jsonl. Task transport is limited to 16 KiB UTF-8 (20 KiB after Bridge scaffolding), and the complete resolved peer argv is checked with Windows list2cmdline UTF-16 semantics before dispatch. Resolved .cmd and .bat shims use an 8,000-unit ceiling below Cmd.exe's documented 8,191-character limit. Retained stdout plus stderr and each persisted transcript are independently bounded at 256 KiB, with transcript wrapper space reserved inside the latter. Each pending stdout record is separately bounded before parsing; an oversized record or retained-output overflow force-terminates the peer tree and records an output-limit incident, including after final flush on timeout or cancellation. Completed Codex command output is absent from the transcript, which instead records aggregated_output_bytes and aggregated_output_sha256; the digest permits comparison with separately retained output but cannot reconstruct evidence and no raw archive is added. Other retained content can still contain task text and provider output, so protect the workspace and apply the project's retention policy before sharing or deleting these files.

Incident records intentionally omit child command arguments and environment data because those values can contain task text or credentials. They retain the classified fault, reason, model, effort, verb, budget, transcript path, and any observed worktree delta from a killed implementation. The predictable health member rejects symlinks and Windows reparse points, opens without following links where the platform provides it, and requires the opened descriptor to be a regular single-link file. Prepared linked or nonregular health members are refused without appending to their targets; a sanitized nonblocking telemetry warning is added to public findings while the original result and primary incident remain available. Nonregular members are rejected before open, and nonblocking POSIX opens prevent a FIFO replacement from stalling before descriptor validation. This check cannot universally contain a new concurrent hard link created after descriptor validation. Setup uses a random per-user HMAC integrity key plus regular non-symlink records beneath the platform user-state root to authenticate and consume approvals, detect recurrence, and lock each target/scope across workspaces. The records retain only allowlisted credential-free identities, fingerprints, timestamps, exit classifications, verification outcomes, and rollback metadata; raw native argv, provider login stdout/stderr, provider tokens or API keys, browser/device codes, account secrets, and raw URLs are never captured. The HMAC key is local integrity state, never a provider credential or substitute for consent. The health record is operational telemetry and should be handled as workspace data.

Result integrity

implement may write the selected worktree. Separately, an approved setup configuration or repair may update the selected host's plugin configuration and its sanitized user-state record; neither authority expands the other. Public status, verdict, findings, files_touched, remaining, and blockers carry decisions, blockers, and remaining work, while transcript-only details hold additional evidence and never hide required work.

The peer/model-to-harness result contains the six decision fields in schemas/envelope.schema.json—status, verdict, findings, files_touched, remaining, and blockers—plus bounded verbose details. The harness persists details in the bounded transcript, strips them from the harness-to-caller public envelope, adds observed metadata and workspace-relative transcript_path and incident references, then validates the complete public result against schemas/harness-envelope.schema.json. Decisions, blockers, and remaining work remain public; transcript-only details provide additional evidence and never hide required work. Timeouts and recursion refusals are constructed by the harness and are never claimed by model output.

Treat the compact envelope, files_touched, transcript paths, cost, timing, and status as evidence to inspect, not as permission to accept a consequential change automatically. Opening the transcript is an opt-in detail inspection; re-read changed files and run the relevant project checks before incorporating an implementation result.

The setup result is a separate lifecycle contract in schemas/setup-result.schema.json. It does not reuse model-run fields or imply that static checks, authentication process exit, session activation, or workspace binding proves successful inference. The deterministic CLI never promotes a point-in-time live result to ready without the loaded-session/workspace evidence it cannot itself observe. ready requires every applicable gate for the selected direction; partial names the exact remaining gate.

Ordinary synchronization is deliberately outside this approval flow: make sync-* invokes only a direct static doctor with bounded read-only probes and structured output. It never invokes a model or setup skill, passes an approval token, authenticates, changes host state, repairs, restarts a session, or makes a provider call. Static sync output cannot claim session-ready or workspace-ready evidence.

Recovery and cancellation

Only one bounded remedy is attempted for a fault. Read-only advise and review requests may receive one timeout or supported-effort retry; a write-capable implement request is never automatically retried — after a timeout or a structured effort failure alike — because edits may already have landed. Setup consumes an approval before its first execution attempt, applies at most one closed remedy for a fault, re-inspects the exact state, and stops on recurrence, unsupported state, approval drift, expiry, replay, or operation failure. Setup rollback is limited to the recorded configuration operation and never touches provider credentials, account state, or raw authentication material. Cancellation writes a contained marker bound to the canonical job identifier; the live supervisor polls that marker and terminates only the child process tree it launched, so lifecycle commands never signal a persisted PID that another process could later reuse. POSIX process-tree termination gives cooperative descendants two seconds to exit, then forces their group even if the leader has already exited. Windows invokes native taskkill /T /F while the leader PID still exists; native Windows process-tree behavior remains an explicit verification surface.

Explicit reverse workspace binding

The installed Bridge MCP backend starts unbound; plugin cwd only locates installed code. bridge_bind_workspace accepts an optional untrusted proposed_workspace suggestion. The user confirms its exact canonical target in one native form that states Codex proposed the folder, so it cannot pass for the user's own typed entry; the proposal alone creates no authority. Empty arguments retain manual folder selection followed by confirmation. Only matched explicit confirmation creates process-local authority. Every advise/review/implement call must supply the current binding_id; missing or stale expectations fail before provider work. Rebinding clears previous authority immediately, including same-folder rebinding and cancellation. Filesystem root, user home itself and every directory above it, installed plugin/cache descendants, and agent configuration homes with their descendants (~/.claude, ~/.codex, and the directories CLAUDE_CONFIG_DIR and CODEX_HOME name) are refused, for typed and proposed folders alike and compared by directory identity as well as spelling; project subdirectories under home are allowed. Folder resolution and directory identity are rechecked before dispatch; changes invalidate binding. Unsupported forms, malformed responses, stale IDs, decline/cancel, and EOF never bind. Binding selects project identity and grants no editing approval, runtime permission, authentication, or paid-call consent. Native Windows directory and cache checks use native path semantics; actual native Windows execution remains a verification surface.

Codex setup records standing transfer consent through interactive bin/bridge_consent.py grant or explicitly authorized grant --approve. The Codex setup skill forwards the noninteractive option only when the user supplies --approve-claude-consent in the current invocation; installation, generic setup approval, repository content, and model-invented flags never supply this authority. The CLI cannot establish the identity of the flag's author; provenance is a caller contract, not an enforced process boundary. The helper changes only its marked additive reviewer-policy block and retains the same file/configuration validation on either route. Claude can read private project context itself; scope and secret exclusions are instructions, not a file allowlist. Calling skills recheck before every dispatch. Revocation remains operator-terminal-confirmed, removes the block, does not cancel active calls, and may require a fresh host session. Stored policy is not effective managed/session authorization; actual denials remain controlling. File guards and the final state comparison detect ordinary drift but do not provide a portable atomic compare-and-swap against hostile same-user processes. See provider consent.