ExecFork command -- branch an existing session into a new one, non-interactively.
Wraps codex exec fork <SESSION_ID> [PROMPT]. Forking copies the source
session's history into a new session and runs the optional prompt there.
The source session is left as it was and stays resumable under its
original ID.
Usage
config = CodexWrapper.Config.new(working_dir: "/path/to/project")
{:ok, fork} =
CodexWrapper.ExecFork.new("019a...-source-session-id")
|> CodexWrapper.ExecFork.prompt("try the other approach")
|> CodexWrapper.ExecFork.fork(config)
fork.session_id # the new session
fork.source_session_id # the session that was forked, unchangedfork/2 is the call to use when the new session ID matters (it
usually does). execute/2, execute_json/2, and stream/2 behave the
same way as they do on CodexWrapper.ExecResume.
Arguments the CLI accepts
Verified against codex-cli 0.149.0. codex exec fork accepts the
codex exec resume flag set without --last and --all, plus
--output-schema. It rejects --sandbox, --profile, and
--full-auto, so sandbox/2 and full_auto/1 emit the config
override -c sandbox_mode="<mode>" the same way ExecResume does.
CLIs without exec fork
An older CLI parses fork as the codex exec prompt and rejects the
session ID as an unexpected argument. execute/2, execute_json/2, and
fork/2 recognize that failure and return
{:error, {:unsupported, :exec_fork}}. supported?/1 checks ahead of
time. stream/2 cannot see the exit status or stderr, so when its
stream ends without a single line it runs supported?/1 and raises
CodexWrapper.UnsupportedError during enumeration if the CLI has no
exec fork.
Summary
Functions
Add a config override (key=value).
Bypass all approvals and sandbox. Use with extreme caution.
Run enabled hooks without requiring persisted hook trust. Use with extreme caution.
Disable a feature.
Enable a feature.
Enable ephemeral mode: the new session is not persisted to disk.
Execute the command synchronously, returning a parsed %Result{}.
Execute with --json and return the parsed %JsonLineEvent{} list.
Fork the session and return the new session ID alongside the source.
Enable full-auto mode.
Do not load user or project execpolicy .rules files.
Do not load $CODEX_HOME/config.toml. Auth still resolves through CODEX_HOME.
Add an image to attach to the prompt sent after forking.
Enable JSON output.
Set the model.
Create a fork of the session with the given ID (a UUID or a thread name).
Set the output-last-message path.
Set the path of a JSON Schema file describing the final response shape.
Set the prompt to send in the new session after forking.
Set the sandbox mode.
Skip the git repo check.
Execute the command and return a lazy Stream of %JsonLineEvent{}.
Error out when config.toml contains fields this Codex version does not recognize.
Return whether the installed CLI has codex exec fork.
Check the source session ID before it reaches the CLI.
Types
@type error() :: {:invalid_session_id, term()} | {:unsupported, :exec_fork} | {:exit, non_neg_integer(), CodexWrapper.Result.t()} | {:missing_session_id, CodexWrapper.Result.t()} | term()
@type fork_result() :: %{ session_id: String.t(), source_session_id: String.t(), result: CodexWrapper.Result.t(), events: [CodexWrapper.JsonLineEvent.t()] }
The outcome of fork/2.
session_id is the new session the CLI created; source_session_id is
the ID that was forked, exactly as passed to new/1.
@type sandbox_mode() :: :read_only | :workspace_write | :danger_full_access
@type t() :: %CodexWrapper.ExecFork{ config_overrides: [String.t()], dangerously_bypass_approvals_and_sandbox: boolean(), dangerously_bypass_hook_trust: boolean(), disabled_features: [String.t()], enabled_features: [String.t()], ephemeral: boolean(), full_auto: boolean(), ignore_rules: boolean(), ignore_user_config: boolean(), images: [String.t()], json: boolean(), model: String.t() | nil, output_last_message: String.t() | nil, output_schema: String.t() | nil, prompt: String.t() | nil, sandbox: sandbox_mode() | nil, session_id: String.t(), skip_git_repo_check: boolean(), strict_config: boolean() }
Functions
Add a config override (key=value).
Bypass all approvals and sandbox. Use with extreme caution.
Run enabled hooks without requiring persisted hook trust. Use with extreme caution.
Hook trust is what stops a repository from running arbitrary commands the user never approved. Only appropriate for automation that already vets where its hooks come from.
Disable a feature.
Enable a feature.
Enable ephemeral mode: the new session is not persisted to disk.
@spec execute(t(), CodexWrapper.Config.t()) :: {:ok, CodexWrapper.Result.t()} | {:error, error()}
Execute the command synchronously, returning a parsed %Result{}.
A non-zero exit is still {:ok, %Result{success: false}}, as with
ExecResume.execute/2, except when the CLI has no exec fork, which
returns {:error, {:unsupported, :exec_fork}}. An invalid session ID
returns {:error, {:invalid_session_id, value}} without spawning.
@spec execute_json(t(), CodexWrapper.Config.t()) :: {:ok, [CodexWrapper.JsonLineEvent.t()]} | {:error, error()}
Execute with --json and return the parsed %JsonLineEvent{} list.
@spec fork(t(), CodexWrapper.Config.t()) :: {:ok, fork_result()} | {:error, error()}
Fork the session and return the new session ID alongside the source.
Forces --json. Returns {:ok, fork_result()} when the CLI exits 0
and reports the new thread ID in its thread.started event. The new ID
comes from the CLI's output, never from the builder, so
source_session_id is always the ID that was forked.
Errors:
{:invalid_session_id, value}-- rejected before spawning{:unsupported, :exec_fork}-- the installed CLI has noexec fork{:exit, code, result}-- the CLI exited non-zero (for example, an unknown source session:no rollout found for thread id){:missing_session_id, result}-- exit 0 without a new thread ID{:timeout, ms}and runner errors, as fromexecute/2
Enable full-auto mode.
Deprecated upstream, and codex exec fork rejects --full-auto. Emits
-c sandbox_mode="workspace-write" instead. An explicit sandbox/2
call is more specific and wins over this.
Do not load user or project execpolicy .rules files.
Do not load $CODEX_HOME/config.toml. Auth still resolves through CODEX_HOME.
Add an image to attach to the prompt sent after forking.
Enable JSON output.
Set the model.
Create a fork of the session with the given ID (a UUID or a thread name).
The ID is checked by validate/1 when the command runs, not here, so a
builder can be assembled from untrusted input and rejected with a
tagged error rather than an exception.
Set the output-last-message path.
Set the path of a JSON Schema file describing the final response shape.
Set the prompt to send in the new session after forking.
The prompt is passed after a -- option terminator, so a prompt that
begins with - or -- reaches the CLI as the prompt rather than as a
flag. A prompt that is exactly - still tells the CLI to read the
prompt from stdin.
@spec sandbox(t(), sandbox_mode()) :: t()
Set the sandbox mode.
Emits -c sandbox_mode="<mode>": codex exec fork rejects --sandbox
with unexpected argument, and the config key takes the same three
values.
Skip the git repo check.
@spec stream(t(), CodexWrapper.Config.t()) :: Enumerable.t()
Execute the command and return a lazy Stream of %JsonLineEvent{}.
Forces --json. Raises ArgumentError for an invalid session ID,
since a stream has no error tuple to return. The new session ID is in
the first thread.started event.
On a CLI without exec fork, enumerating the stream raises
CodexWrapper.UnsupportedError with capability: :exec_fork, the
stream counterpart of {:error, {:unsupported, :exec_fork}}. Only a
stream that ends without output pays for the supported?/1 check; a
stream that yields a line, or that the consumer halts early, never
runs it. An empty stream from a CLI that does have exec fork (for
example, an unknown source session) still ends without raising.
Error out when config.toml contains fields this Codex version does not recognize.
@spec supported?(CodexWrapper.Config.t()) :: boolean()
Return whether the installed CLI has codex exec fork.
Runs codex exec fork --help, which needs no authentication and starts
no session.
Check the source session ID before it reaches the CLI.
Accepts a non-empty string with no leading or trailing whitespace, no
control characters, and no leading - (which the CLI would parse as a
flag). Both UUIDs and thread names pass. Anything else returns
{:error, {:invalid_session_id, value}}.