@coderifts/agent-hooks
v0.2.1
Published
Ask CodeRifts before an agent writes a contract artifact (OpenClaw before_tool_call and Claude Code PreToolUse).
Downloads
285
Readme
CodeRifts agent hooks (@coderifts/agent-hooks)
Asks CodeRifts before an agent writes a contract artifact — an OpenAPI, AsyncAPI, GraphQL,
protobuf or MCP manifest file. Hosts today: OpenClaw before_tool_call and Claude Code
PreToolUse. Not built: the Docker MCP Gateway before:exec interceptor — that path
was measured (empty stdout = pass, CallToolResult JSON = block, HTTP 4xx empty = fail-open)
and has no adapter in this package yet.
This is the host-adapter package. It is not @coderifts/agent-guard (the wrapWithGuard
library) and not coderifts/contract-gate (the GitHub Action). The npm name used to be
@coderifts/openclaw-plugin; that name is wrong now that more than one host is wired.
The same gate.js drives every host. The gate makes no policy decisions of its own. It
recognises the file, sends the before/after to CodeRifts, and maps the answer back.
Everything it cannot map, it puts to a human.
What it does
| CodeRifts execution_action | OpenClaw | Claude Code PreToolUse |
| --- | --- | --- |
| CONTINUE, CONTINUE_WITH_MONITORING | pass through | exit 0, empty stdout (never allow) |
| REQUEST_APPROVAL | requireApproval | JSON permissionDecision: "ask" |
| STOP | block, with the reason and its limits | JSON permissionDecision: "deny" |
| anything else, or no answer | requireApproval | exit 2 + stderr (host is fail-open) |
What it does not do
- It does not gate what it does not recognise. A tool call writing
README.mdpasses untouched. The recognised set is a short, explicit list ingate.js; a gate that guesses at every file either blocks documentation or waves through a schema. - It does not turn a risk number into a verdict. Without an API key CodeRifts answers in
analyzemode, which returnsauthorization_effect: "NONE"andmay_execute: false— risk information and no decision. The gate reports that and asks a human. It never reads a high score as a block or a low one as a pass. - It does not lock the file. The bytes checked are the bytes in the tool params at the moment of the call. Nothing here holds them between the answer and the write.
Every refusal says so in its own text, so an agent reading the transcript can see how far the answer reaches:
CodeRifts refused this contract change.
Reason: endpoint_removed — ENDPOINT_REMOVAL
Proves: the change set as this call would leave it was refused by CodeRifts under operation "tool_call".
Does not prove:
- that the bytes finally written are the bytes checked — nothing here locks the file between this answer and the write
- that the other tool calls in this run were checked — each call is judged alone
- that a contract artifact this gate does not recognise was seen at allFail-closed
If CodeRifts is unreachable, times out, or answers something the gate cannot read, the result is not a pass. Not knowing is not permission.
The gate's own budget defaults to 5000 ms.
On OpenClaw, that is well under the host's 15000 ms before_tool_call budget, so a slow
answer becomes a readable approval request, not an opaque host denial. The host is fail-closed.
On Claude Code, the host is fail-open: a timed-out command hook, exit 1, invalid JSON,
or a missing script lets the tool run. The adapter therefore maps those failures to exit 2.
The hook entry sets "timeout": 8 (seconds — Claude Code's unit, not milliseconds) so the
gate's 5000 ms abort can finish first. If node itself hangs past 8 s, Claude Code still
lets the write through. That host behaviour cannot be fixed in this package.
Shell writes (Claude Code)
The Claude Code matcher is Write|Edit|MultiEdit|Bash. A shell command cannot be gated — the
hook never sees the bytes it would leave — so the hook does something narrower: a Bash command
that names a recognised contract file and has a write shape (a redirect, tee, sed -i,
perl -i, cp/mv/rm, git checkout|restore|apply, open(…, "w"), curl -o, …) is
denied with a pointer to the Write or Edit tool, where the gate does see the change.
Reads (cat, git diff, grep, oasdiff) pass. Nothing is sent to CodeRifts for a Bash call.
It is deny, not ask: a hook ask was reported to override a settings deny rule
(anthropics/claude-code #39344), and an escalation that can downgrade someone else's deny is
not one.
It is not a parser. A write that does not name the file (python script.py, a variable, a
glob) is not recognised, and every shell refusal says so:
Does not prove:
- that a contract file written by a shell command this hook did not recognise was seen at all
Edit and MultiEdit
new_string is a fragment, not the file. The gate reads the file on disk and sends it with the
edit applied (MultiEdit: every edit, in order). An edit whose old_string is not in the file is
not guessed: exit 2.
Configuration
{
"plugins": {
"entries": {
"coderifts-contract-gate": {
"config": {
"apiKey": "...",
"endpoint": "https://app.coderifts.com/mcp",
"timeoutMs": 5000,
"operation": "tool_call"
}
}
}
}
}apiKey is optional in the schema and load-bearing in practice: without it there is no decision.
preflight_mode=authorize requires a verified issuer, so an unkeyed gate runs in analyze mode and
every gated call ends at a human. That is safe and it is noisy; the key is what makes it useful.
There is no setting that makes an unanswered call pass. That absence is the design.
Hook placement
Registered on before_tool_call at priority: 100. OpenClaw runs handlers in descending priority
and keeps only the first requireApproval, so a gate that runs late can have its question
dropped by an earlier plugin. A block is sticky and survives regardless of order.
Install — OpenClaw
openclaw plugins install @coderifts/agent-hooks(OpenClaw still records the plugin under the runtime id coderifts-contract-gate — that is
the OpenClaw config key, not the GitHub Action.)
Install — Claude Code
Copy claude-code/settings.snippet.json into .claude/settings.json (or merge the
hooks key), after npm install @coderifts/agent-hooks. Optional env:
CODERIFTS_API_KEY, CODERIFTS_ENDPOINT, CODERIFTS_TIMEOUT_MS (milliseconds, gate
budget; default 5000).
Plugin shape (for the existing coderifts marketplace): .claude-plugin/plugin.json +
hooks/hooks.json. Add claude-code/marketplace-entry.json to
coderifts/api-governance .claude-plugin/marketplace.json plugins array — that
catalog today lists only api-governance. Then:
claude plugin marketplace update coderifts
claude plugin install agent-hooks@coderiftsDo not use permissionDecision: allow for CONTINUE. Empty exit 0 leaves the normal
permission prompt in place.
Test
npm testnpm test includes a pack-then-install case. In-repo imports cannot see a
files allowlist that dropped a relative module — that is how 0.1.0 shipped
without gate.js. The pack test also requires claude-code/hook.mjs. Do not
skip it.
