> ## Documentation Index
> Fetch the complete documentation index at: https://docs.theauth.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Explain a decision

> Ask why authorize() would allow or deny a call, with the matched rule, delegation link, constraint checks and hook outcome, and no side effects.

## What it does

`theauth.explain(agentId, action, resource, options?)` mirrors `theauth.authorize()` and returns the reasoning instead of just the verdict. It answers questions like "which rule let this through?", "which delegation carried it?" and "why was this denied?".

It only reads. It writes no audit rows, does not increment rate limit counters, does not spend approvals, and never caches.

```ts theme={"dark"}
const result = await theauth.explain("agent_123", "read", "mcp:github:repos", {
  context: { ip: "10.1.2.3" },
});

if (!result.success) throw new Error(result.error.message);
const e = result.data;

console.log(e.decision, e.code, e.reason);
console.log(e.matchedGrant?.source, e.matchedGrant?.link?.delegationId);
console.log(e.constraints);
```

The call returns a `Result`. A denial is a successful explanation (`decision: "deny"`). A database failure returns `EXPLAIN_LOAD_FAILED`, and empty input returns `EXPLAIN_INVALID_INPUT`.

## Result

| Field | Meaning |
| - | - |
| `decision` | `allow`, `deny` or `escalate` |
| `allowed` | `true` only for `allow` |
| `code` | The same [decision code](/errors) `authorize()` would return, such as `no_permission`, `rate_limited` or `hook_denied` |
| `reason` | Human readable explanation |
| `agent` | Id, name, owner, status and expiry. `null` when the agent does not exist (code `agent_not_found`) |
| `delegation` | The chain from the root human to the agent, plus every inbound grant |
| `matchedGrant` | The rule that decided, or that came closest. Includes `source` (`own` or `delegation`), `index`, `resourceMatch`, `actionMatch` and, for delegated rules, the `link` that carried it |
| `constraints` | Checks on the matched rule, in order: rate limit, argument patterns, approval, time window, IP allowlist |
| `attempts` | Which permission sets were tried (own, then delegated) and how each ended |
| `nearMisses` | When no grant matched, the closest grants that failed on resource or action (see below). Empty when `matchedGrant` is set |
| `hook` | Whether `beforeAuthorize` is configured, whether it ran, and what it said |
| `trace` | The full step list, the same `TraceStep` type as the [simulator](/simulator), with a `hook` stage |

## Near misses

When no grant matches, `matchedGrant` is absent and `nearMisses` lists up to three grants that almost allowed the call, best first. Own grants and delegated grants are both considered. A grant that matched on resource and action but failed a constraint is reported as `matchedGrant` instead, so `nearMisses` stays empty then.

```ts theme={"dark"}
// agent holds { resource: "mcp:github:issues", actions: ["read"] }
const e = (await theauth.explain(id, "read", "mcp:github:create_issue")).data!;
e.nearMisses[0];
// {
//   source: "own",
//   grant: { resource: "mcp:github:issues", actions: ["read"] },
//   mismatch: ["resource"],
//   hint: 'resource "mcp:github:issues" does not cover "mcp:github:create_issue"; "mcp:github:*" would',
//   score: 0.8,
// }
```

| Field | Meaning |
| - | - |
| `source` | `own` or `delegation` |
| `delegationId` | The delegation that carried the grant. Only for `delegation` |
| `grant` | The grant's `resource` and `actions` |
| `mismatch` | `resource`, `action`, or both |
| `hint` | A plain sentence saying what would have matched |
| `score` | 0 to 1: 0.6 times the share of leading `:` segments the resource pattern has in common with the requested resource, plus 0.4 when the action matched |

Near misses are computed from the same permission lists, with no extra queries and no side effects. To turn explain into CI checks, see [policy testing](/policy-testing).

## Semantics

`explain()` follows `authorize()`: the first matching rule wins, own permissions are tried before delegated ones, and when both deny the own verdict stands. When a delegated rule matched but failed a constraint, `matchedGrant` and `constraints` show that rule and `reason` says so, even though the code stays the one `authorize()` returns.

A rule with `requireApproval` reports `decision: "escalate"` and code `approval_required`. Explain cannot spend an approval.

The unified `policy.evaluate()` engine (deny overrides, risk, taint) is not explained yet. The `engine` option is reserved for it and only accepts `"authorize"`.

## Options

| Option | Default | Meaning |
| - | - | - |
| `arguments` | none | Tool arguments checked against `allowedArgPatterns` |
| `context` | none | Request metadata. `ip` is checked against `ipAllowlist` |
| `at` | now | Evaluate time windows at this instant |
| `runHooks` | `false` | Call `beforeAuthorize` with `dryRun: true` and `source: "explain"` |

## Hooks

Hooks can call external services, so `explain()` skips `beforeAuthorize` unless you pass `runHooks: true`. When skipped, `hook.configured` is `true` and `hook.evaluated` is `false`, which tells you the live answer may differ.

With `runHooks: true` the hook runs through the same timeout and fail-closed handling as `authorize()`. It receives `dryRun: true`, so a hook that writes records or notifies someone should check that flag and stay quiet. A deny, escalate, timeout or error from the hook is reflected in `decision`, `code` and `hook`.

## Related

* [Permission simulator](/simulator) for what-if overrides, matrices and access reviews
* [Hooks](/hooks) for `beforeAuthorize` and `dryRun`


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.