# `ObanClaude.Args`
[🔗](https://github.com/genagent/oban_claude/blob/v0.10.2/lib/oban_claude/args.ex#L1)

Build a claude job's args map without knowing `claude_wrapper` or the CLI.

`new/1` is the first-class front door: a keyword list with atom keys and native
Elixir values in, the string-keyed JSON map Oban stores out. It is the
preferred way to construct args; `ObanClaude.run/2` and `ObanClaude.Worker`
still accept a raw string-keyed map directly as the low-level escape hatch.

    ObanClaude.Args.new(prompt: "summarize the repo",
                        working_dir: "/repo",
                        permission_mode: :plan)
    #=> %{"prompt" => "summarize the repo",
    #     "working_dir" => "/repo",
    #     "permission_mode" => "plan"}

The result is a plain map, so it drops straight into a worker:

    MyApp.ClaudeJob.new(ObanClaude.Args.new(prompt: "...")) |> Oban.insert()

## Worker defaults

`defaults/1` is the same builder with `:prompt` optional, for the worker's
`:args` (the constant, prompt-less config). It evaluates at compile time, so it
works directly in the `use`:

    use ObanClaude.Worker,
      queue: :claude,
      args: ObanClaude.Args.defaults(working_dir: ".", model: "sonnet",
                                     permission_mode: :bypass_permissions)

## Job metadata

The `:meta` option carries non-claude values (an issue number, a correlation id)
through to the Oban job args, where `handle_result/2` and telemetry can read
them. It is merged flat into the map (keys stringified) and is not validated as
a claude option:

    ObanClaude.Args.new(prompt: "...", meta: %{"issue" => "173"})
    #=> %{"prompt" => "...", "issue" => "173"}

Because it is merged flat, `:meta` has two guardrails (both raise at build
time):

  * A meta key may not collide with a claude option name (or `"prompt"`) --
    otherwise it would become an unvalidated query option and could override a
    worker default across the merge. Rename the key, or set the option directly.
  * Meta values must be JSON-encodable (Oban stores args as JSON). A tuple or a
    struct without a `Jason.Encoder` raises here, naming the key, rather than
    deep inside `Oban.insert`.

One caveat the guardrails cannot catch: an **atom** meta value is valid JSON
but round-trips back as a *string* (`:high` in, `"high"` out). Hand-built
`%Oban.Job{args: ...}` test jobs skip that round-trip, so a `handle_result/2`
clause matching on the atom passes in tests and silently never matches in
production. Use string meta values, and prefer `Oban.Testing.perform_job/3`
(which JSON-recodes args) over a hand-built job in worker tests.

## Validation

Unlike the raw-map path (which forwards known keys and silently ignores the
rest), the builder validates against the schema below: unknown keys raise, and
each option's type (including the `:permission_mode` and `:effort` vocabularies)
is checked. Errors surface at construction / enqueue time rather than when the
job runs.

## Options

* `:prompt` (`t:String.t/0`) - Required. The claude prompt. The only required option for `new/1`.

* `:model` (`t:String.t/0`) - Model name, e.g. `"sonnet"` or `"opus"`.

* `:fallback_model` (`t:String.t/0`) - Model to fall back to if the primary is unavailable.

* `:working_dir` (`t:String.t/0`) - Directory claude runs in.

* `:binary` (`t:String.t/0`) - Path to the `claude` CLI binary. Pin a known-good version per worker (e.g. in `defaults/1`) so a PATH auto-update mid-batch can't drift the run -- pairs well with `hermetic`. (The wrapper's `:bundled` mode is not exposed here; use a custom `:query_fun` for that.)

* `:add_dir` - Extra directories claude may access -- a single path or a list of paths.

* `:system_prompt` (`t:String.t/0`) - Replace claude's system prompt.

* `:append_system_prompt` (`t:String.t/0`) - Append to claude's system prompt.

* `:system_prompt_file` (`t:String.t/0`) - Replace claude's system prompt from a file path (avoids the argv size limit a large inline `system_prompt` would hit).

* `:append_system_prompt_file` (`t:String.t/0`) - Append to claude's system prompt from a file path.

* `:permission_prompt_tool` (`t:String.t/0`) - Delegate permission decisions to a custom MCP tool (e.g. `"mcp__gate__approve"`) for programmatic headless gating.

* `:max_thinking_tokens` (`t:pos_integer/0`) - Cap the thinking-token budget -- finer control than `effort`.

* `:permission_mode` - How claude handles tool-permission prompts.

* `:allowed_tools` (list of `t:String.t/0`) - Whitelist of tools claude may use.

* `:disallowed_tools` (list of `t:String.t/0`) - Blacklist of tools claude may not use.

* `:mcp_config` (list of `t:String.t/0`) - MCP server config file paths.

* `:strict_mcp_config` (`t:boolean/0`) - Use only MCP servers supplied through `mcp_config` (`--strict-mcp-config`). This seals MCP discovery only; use `hermetic` when the run must also exclude ambient hooks, instructions and settings.

* `:agent` (`t:String.t/0`) - Named agent/subagent to run as.

* `:effort` - Reasoning effort level.

* `:max_turns` (`t:pos_integer/0`) - Cap on agent turns in the run.

* `:max_budget_usd` - Cost ceiling for the run, in USD.

* `:timeout` (`t:pos_integer/0`) - Subprocess timeout in milliseconds.

* `:json_schema` (`t:String.t/0`) - An inline JSON Schema *string* for structured output, passed verbatim to the CLI's `--json-schema` (not a file path -- read the file yourself and pass its contents).

* `:worktree` - Run in a git worktree (`--worktree`). `true` for an ephemeral worktree, a string for a named one (reusable across jobs -- e.g. `"issue-173"`). Requires `working_dir` to be a git repo. Recommended for full-auto workers that write to a repo -- set it in `defaults/1`.

* `:setting_sources` (`t:String.t/0`) - Comma-separated Claude setting sources to load (`user`, `project`, and/or `local`). For example, `"project,local"` keeps repository instructions and settings while excluding the user's global config. Unlike `hermetic`, this controls only setting-source discovery.

* `:hermetic` - Seal the ambient `~/.claude` config so the run's surface is exactly what these args set (`--setting-sources`, `--strict-mcp-config`, `--exclude-dynamic-system-prompt-sections`; auth untouched). `true` is an alias for `:full`. `:full` drops user + project + local ambient config; `:project` seals project + local but keeps the user's global `~/.claude`. Recommended in `defaults/1` for reproducible server runs that should not depend on the host's config.

* `:resume` (`t:String.t/0`) - Resume a prior claude session by id (`--resume`). The id comes from a previous run -- `ObanClaude.session_id/1` reads it off a `%Result{}` or a rail-stop `%Error{}`. The canonical use is the resume-after-rail-stop recipe: a follow-on job with `resume:` + the same *named* `worktree` continues a stopped run cheaply. Session state is node-local (see the "session-resume handoff" pattern in the Agent worker patterns guide).

* `:session_id` (`t:String.t/0`) - Pin the session id for this run (`--session-id`), so a known id can be resumed later. Distinct from `resume`, which *continues* an existing id.

* `:no_session_persistence` (`t:boolean/0`) - Do not persist this run's session transcript under `~/.claude` (`--no-session-persistence`). Recommended for fire-and-forget batch workers: it stops unattended runs accumulating transcripts (disk growth and plaintext data at rest). Mutually exclusive with the resume recipe -- it deletes the transcript `resume` would need.

* `:fork_session` (`t:boolean/0`) - Fork a new session from the resumed one rather than continuing it in place (`--fork-session`), so the original transcript is left untouched.

* `:meta` - Non-claude metadata merged into the args map untouched (keys stringified). For values `handle_result/2` or telemetry needs -- an issue number, a correlation id. Explicit claude options win on a key collision. Note: meta set as a *worker default* (via `defaults/1`) reaches telemetry (the merged args) but NOT `handle_result/2`, which sees only the job's own args -- put meta a handler reads on the job, not the worker default.

# `t`

```elixir
@type t() :: %{optional(String.t()) =&gt; term()}
```

The string-keyed args map the builder produces and `ObanClaude.run/2`
consumes -- what Oban serializes as the job's args. Keys are strings (claude
options plus any stringified `:meta`); values are JSON-clean.

# `defaults`

```elixir
@spec defaults(keyword()) :: t()
```

Build a worker-level defaults map: the same as `new/1`, but `:prompt` is
optional (worker `:args` are the prompt-less config the per-job args fill in).

Because it evaluates at compile time, it can be used directly in the worker's
`use` (see "Worker defaults" above).

# `keys`

```elixir
@spec keys() :: [atom()]
```

The options the builder accepts.

# `new`

```elixir
@spec new(keyword()) :: t()
```

Build a string-keyed args map from a keyword list of atom-keyed options.

Validates `opts` against the schema documented above (see "Options"). Raises
`NimbleOptions.ValidationError` on a missing `:prompt`, an unknown key, or a
value of the wrong type. Returns the map Oban serializes as the job's args.

---

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