# `ExAthena.Permissions`
[🔗](https://github.com/udin-io/ex_athena/blob/v0.7.1/lib/ex_athena/permissions.ex#L1)

Decides whether a tool call is allowed.

Every tool call runs through `check/4` before execution. The check combines
four sources — in this order, first decisive wins:

  1. **`disallowed_tools`** — an explicit blocklist. Always denies.
  2. **`allowed_tools`** — an explicit allowlist. If non-nil, denies anything
     not in it.
  3. **`phase`** — the current permission mode:
     * `:plan` — read-only. Writes and shell execution are denied.
     * `:default` — read + write. `can_use_tool` callback (if supplied) can
       ask the user.
     * `:accept_edits` — auto-allow Read/Edit/Write/Glob/Grep/WebFetch
       + `plan_mode` / `spawn_agent`; still consults `can_use_tool` for
       everything else (e.g. `bash`, custom tools).
     * `:trusted` — skip the `can_use_tool` callback for every tool.
       Still respects the disallow / allowlist by default; pass
       `respect_denylist: false` to disable that too (equivalent to
       `:bypass_permissions`).
     * `:bypass_permissions` — everything allowed without asking.
  4. **`can_use_tool`** — caller-supplied callback (only in `:default`
     and unconditionally-allowed-tool slots of `:accept_edits`).

The `can_use_tool` callback is a function `(tool_name, arguments, ctx ->
:allow | :deny | {:deny, reason})` that the loop calls in `:default` mode
for anything the caller marked as sensitive. See `Permissions.Opts` below.

Reserved name: `:auto` is reserved for the future ML safety classifier
mode the Claude Code paper describes; do not use it.

## Deny-first ordering

The check chain is **disallowed → allowed → phase → callback**, with the
first decisive answer winning. A blocked tool stays blocked even when
`:bypass_permissions` would otherwise allow everything:

    iex> alias ExAthena.{Permissions, ToolContext}
    iex> alias ExAthena.Messages.ToolCall
    iex> tc = %ToolCall{id: "1", name: "bash", arguments: %{}}
    iex> ctx = ToolContext.new(cwd: "/tmp", phase: :bypass_permissions)
    iex> Permissions.check(tc, ctx, %{disallowed_tools: ["bash"]})
    {:deny, {:disallowed, "bash"}}

Likewise, an allowlist denies everything outside it even if a callback
would have allowed:

    iex> alias ExAthena.{Permissions, ToolContext}
    iex> alias ExAthena.Messages.ToolCall
    iex> tc = %ToolCall{id: "1", name: "bash", arguments: %{}}
    iex> ctx = ToolContext.new(cwd: "/tmp", phase: :default)
    iex> opts = %{allowed_tools: ["read"], can_use_tool: fn _, _, _ -> :allow end}
    iex> Permissions.check(tc, ctx, opts)
    {:deny, {:not_in_allowlist, "bash"}}

# `opts`

```elixir
@type opts() :: %{
  optional(:phase) =&gt; ExAthena.ToolContext.phase(),
  optional(:allowed_tools) =&gt; [String.t()] | nil,
  optional(:disallowed_tools) =&gt; [String.t()] | nil,
  optional(:can_use_tool) =&gt; (String.t(), map(), ExAthena.ToolContext.t() -&gt;
                                result()),
  optional(:respect_denylist) =&gt; boolean()
}
```

# `result`

```elixir
@type result() :: :allow | {:deny, reason :: term()}
```

# `check`

```elixir
@spec check(ExAthena.Messages.ToolCall.t(), ExAthena.ToolContext.t(), opts()) ::
  result()
```

Check whether `tool_call` is allowed under `opts`. Returns `:allow` or
`{:deny, reason}`.

# `readonly_tools`

```elixir
@spec readonly_tools() :: [String.t()]
```

Static list of read-only tool names the `:plan` phase permits.

---

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