CodexWrapper.ExecFork (CodexWrapper v0.5.1)

Copy Markdown View Source

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, unchanged

fork/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

error()

@type error() ::
  {:invalid_session_id, term()}
  | {:unsupported, :exec_fork}
  | {:exit, non_neg_integer(), CodexWrapper.Result.t()}
  | {:missing_session_id, CodexWrapper.Result.t()}
  | term()

fork_result()

@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.

sandbox_mode()

@type sandbox_mode() :: :read_only | :workspace_write | :danger_full_access

t()

@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

config(e, kv)

@spec config(t(), String.t()) :: t()

Add a config override (key=value).

dangerously_bypass_approvals_and_sandbox(e)

@spec dangerously_bypass_approvals_and_sandbox(t()) :: t()

Bypass all approvals and sandbox. Use with extreme caution.

dangerously_bypass_hook_trust(e)

@spec dangerously_bypass_hook_trust(t()) :: t()

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(e, feature)

@spec disable(t(), String.t()) :: t()

Disable a feature.

enable(e, feature)

@spec enable(t(), String.t()) :: t()

Enable a feature.

ephemeral(e)

@spec ephemeral(t()) :: t()

Enable ephemeral mode: the new session is not persisted to disk.

execute(exec, config)

@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.

execute_json(exec, config)

@spec execute_json(t(), CodexWrapper.Config.t()) ::
  {:ok, [CodexWrapper.JsonLineEvent.t()]} | {:error, error()}

Execute with --json and return the parsed %JsonLineEvent{} list.

fork(exec, config)

@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 no exec 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 from execute/2

full_auto(e)

@spec full_auto(t()) :: t()

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.

ignore_rules(e)

@spec ignore_rules(t()) :: t()

Do not load user or project execpolicy .rules files.

ignore_user_config(e)

@spec ignore_user_config(t()) :: t()

Do not load $CODEX_HOME/config.toml. Auth still resolves through CODEX_HOME.

image(e, path)

@spec image(t(), String.t()) :: t()

Add an image to attach to the prompt sent after forking.

json(e)

@spec json(t()) :: t()

Enable JSON output.

model(e, model)

@spec model(t(), String.t()) :: t()

Set the model.

new(session_id)

@spec new(String.t()) :: t()

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.

output_last_message(e, path)

@spec output_last_message(t(), String.t()) :: t()

Set the output-last-message path.

output_schema(e, path)

@spec output_schema(t(), String.t()) :: t()

Set the path of a JSON Schema file describing the final response shape.

prompt(e, prompt)

@spec prompt(t(), String.t()) :: t()

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.

sandbox(e, mode)

@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_git_repo_check(e)

@spec skip_git_repo_check(t()) :: t()

Skip the git repo check.

stream(exec, config)

@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.

strict_config(e)

@spec strict_config(t()) :: t()

Error out when config.toml contains fields this Codex version does not recognize.

supported?(config)

@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.

validate(exec_fork)

@spec validate(t()) :: :ok | {:error, {:invalid_session_id, term()}}

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}}.