# `CodexWrapper.ExecFork`
[🔗](https://github.com/genagent/codex_wrapper_ex/blob/v0.6.0/lib/codex_wrapper/exec_fork.ex#L1)

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

# `error`

```elixir
@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`

```elixir
@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`

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

# `t`

```elixir
@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()
}
```

# `config`

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

Add a config override (key=value).

# `dangerously_bypass_approvals_and_sandbox`

```elixir
@spec dangerously_bypass_approvals_and_sandbox(t()) :: t()
```

Bypass all approvals and sandbox. Use with extreme caution.

# `dangerously_bypass_hook_trust`

```elixir
@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`

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

Disable a feature.

# `enable`

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

Enable a feature.

# `ephemeral`

```elixir
@spec ephemeral(t()) :: t()
```

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

# `execute`

```elixir
@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`

```elixir
@spec execute(t(), CodexWrapper.Config.t(), keyword()) ::
  {:ok, CodexWrapper.Result.t()} | {:error, term()}
```

Execute with optional early session observation. See
`CodexWrapper.ObservedExecution` for options, ordering and raw-output semantics.
An empty options list delegates to legacy `execute/2`.

# `execute_json`

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

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

# `execute_json`

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

Execute with `--json` and optional session observation, then parse stdout.
See `execute/3` for execution options.

# `fork`

```elixir
@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`

# `fork`

```elixir
@spec fork(t(), CodexWrapper.Config.t(), keyword()) ::
  {:ok, fork_result()} | {:error, error()}
```

Fork with optional early observation and retain the existing fork result shape.
Observed identity is accepted only from valid stdout `thread.started` events.
See `CodexWrapper.ObservedExecution` for execution options.

# `full_auto`

```elixir
@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`

```elixir
@spec ignore_rules(t()) :: t()
```

Do not load user or project execpolicy `.rules` files.

# `ignore_user_config`

```elixir
@spec ignore_user_config(t()) :: t()
```

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

# `image`

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

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

# `json`

```elixir
@spec json(t()) :: t()
```

Enable JSON output.

# `model`

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

Set the model.

# `new`

```elixir
@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`

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

Set the output-last-message path.

# `output_schema`

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

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

# `prompt`

```elixir
@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`

```elixir
@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`

```elixir
@spec skip_git_repo_check(t()) :: t()
```

Skip the git repo check.

# `stream`

```elixir
@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`

```elixir
@spec strict_config(t()) :: t()
```

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

# `supported?`

```elixir
@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`

```elixir
@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}}`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
