@intended-inc/claude-code-hook
v0.3.1
Published
Claude Code PreToolUse hook — govern every tool call through Intended's authority layer. Fail-closed when unconfigured (opt out with INTENDED_HOOK_UNCONFIGURED=allow) and on DENY / unreachable / non-2xx. npx-invokable from .claude/settings.json.
Readme
@intended-inc/claude-code-hook
A Claude Code PreToolUse hook that governs every tool call through Intended's authority layer — "no token, no action" before execution.
Wire it (.claude/settings.json)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": "npx -y @intended-inc/claude-code-hook" }
]
}
]
}
}Behaviour
| State | Result |
|---|---|
| Unconfigured (no INTENDED_API_KEY / INTENDED_TENANT_ID, or either is blank) | deny every governed tool call, with a message naming what is missing and how to fix it |
| Unconfigured with INTENDED_HOOK_UNCONFIGURED=allow | allow, ungoverned, with a warning on every call (for local trials only) |
| Hook input that is not a JSON object, or any error inside the hook | deny (the hook always exits 0 with an answer, so a crash never lets a tool run) |
| 200 APPROVED | allow |
| 202 ESCALATED | wait for the approver in Intended (see below) |
| 403 DENIED | deny (with reason) |
| Unreachable, non-2xx (401/429/5xx/…), or unrecognized body | deny (fail-closed) — never falls through to allow |
What happens on ESCALATE
The hook waits for a human approver to decide in the Intended console, for up to
INTENDED_HOOK_ESCALATION_WAIT_SECONDS (default 45).
- Approved in time: allow.
- Rejected, or the approval request expired: deny with the reason.
- Still undecided: deny (fail-closed) with a message giving the intent id: ask an
approver to approve it in the Intended console, then retry the same tool call. The
pending intent is saved in
~/.intended/pending.json, keyed by a hash of the tool name and arguments (bounded; expired entries are dropped). When Claude retries the same tool call with the same arguments, the hook checks that intent instead of creating a new escalation, and allows the call once it is approved.
Set INTENDED_HOOK_ON_PENDING=ask to defer a still-undecided call to Claude Code's own
permission prompt instead of denying it.
A wait that fails with 401, 403 (the key needs the intent:read scope), 404, or an
unrecognised response is a deny. Only an approval allows.
Claude Code hooks time out after 60 seconds by default. To wait longer than about 55
seconds, raise the hook's timeout (in seconds) above the wait:
{ "type": "command", "command": "npx -y @intended-inc/claude-code-hook", "timeout": 300 }and set INTENDED_HOOK_ESCALATION_WAIT_SECONDS below it (for example 280).
Config (env): INTENDED_API_KEY (scopes intent:write and intent:read), INTENDED_TENANT_ID,
INTENDED_API_URL (optional), INTENDED_HOOK_ESCALATION_WAIT_SECONDS (default 45),
INTENDED_HOOK_ON_PENDING (deny or ask, default deny), INTENDED_HOOK_UNCONFIGURED
(set to allow to let tool calls through ungoverned while the hook is unconfigured; anything
else, or unset, denies).
Upgrading from 0.2: an unconfigured hook used to allow every call silently. It now denies.
Set the key and tenant, or set INTENDED_HOOK_UNCONFIGURED=allow while you try it out.
Attribution: actor = claude-code:<tool>. Last decision → ~/.intended/last-decision.json.
It's a CLI (the SDK is the library). Apache-2.0.
