Show Claude Code the Decisions before it edits
Claude Code can ask Smeldr before it edits a file. Before the first edit of a file an Area covers, a hook holds the edit back and shows the agent the Decisions in force for that file, with their text. If the change keeps to them, the agent makes the same edit again and it goes through. If it would go against one, the agent is told to stop and ask you. The hook does not judge the change; the agent does.
You need the server and smeldr-cli from Getting started, and the places from Set up the example places: the Area ARE1 "Billing", which covers internal/billing/** in acme/app, and three Decisions in force for it: D2 in the Organization, D3 in the Domain and D4 "Invoices are immutable" in the Area. On your server the hook lists your own Areas and Decisions.
Commands shown once run unchanged in bash and in Windows PowerShell, with the files named as in Getting started. cmd.exe is not covered. The steps were run as written on Linux and in Windows PowerShell on Windows 11; the Claude Code part was run on Windows 11 only.
Try the hook without Claude Code
The hook is smeldr-cli hook pre-edit. It reads what Claude Code sends before an edit on its input and writes its answer on its output, so you can try it by hand. In the directory where you ran Getting started, with the server running, hand it an edit of internal/billing/invoice.go:
echo '{"session_id":"try-1","tool_name":"Edit","tool_input":{"file_path":"internal/billing/invoice.go"}}' | ./smeldr-cli hook pre-edit --repo acme/app --root "$PWD"
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Smeldr: before you edit internal/billing/invoice.go, this is what is decided for it in acme/app (the Smeldr instance at http://localhost:8080).\nArea ARE1 Billing (approval: requires-approval)\nDecision D2 (organization ORG1): Store timestamps in UTC, with the offset kept\n Text: Every timestamp we store or send is in UTC. Where the local offset matters, it is stored next to it.\nDecision D3 (domain DOM2): Payments use idempotency keys\n Text: Every payment request carries an idempotency key, so a retry never charges twice.\nDecision D4 (area ARE1): Invoices are immutable\n Text: An issued invoice is never changed; a correction is a new invoice.\nThese are the Decisions in force for this file. If your change keeps to them, make the same edit again now: this hook lets it through. If it would go against one, stop and ask the user instead. This hook does not check the change against them."}}
The edit is denied, and the reason is what the agent reads: the Area, and each Decision in force with its text. A long text is shortened, and the text is given for the first ten Decisions; any more are listed by title. --root is the project's top directory, which the path is taken relative to; in Claude Code the hook takes it from the session.
Run the same line again. The hook remembers what it showed in this session (try-1), so the same edit is now allowed:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}
A new Area or Decision on the file denies it once more, and so does a new session name. A file no Area covers is allowed at once, and the agent is told why:
echo '{"session_id":"try-1","tool_name":"Edit","tool_input":{"file_path":"docs/readme.md"}}' | ./smeldr-cli hook pre-edit --repo acme/app --root "$PWD"
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":"Smeldr named no Area for docs/readme.md in acme/app to this token, so it showed no Decision for it. An Area this token may not read is left out."}}
Now stop the server (Ctrl+C in its terminal) and hand it a covered edit in a new session:
echo '{"session_id":"try-2","tool_name":"Edit","tool_input":{"file_path":"internal/billing/invoice.go"}}' | ./smeldr-cli hook pre-edit --repo acme/app --root "$PWD"
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Smeldr: the server at http://localhost:8080 did not answer which Decisions apply to internal/billing/invoice.go: request: Post \"http://localhost:8080/mcp/message\": dial tcp [::1]:8080: connect: connection refused. Edits are held until it answers. To edit anyway, the user can change --on-error to allow in the hook's command in .claude/settings.json."}}
On Windows the cause reads connectex: No connection could be made because the target machine actively refused it. instead of connect: connection refused.
With no answer, the edit is held. The same holds for a refusal, a server that does not answer within --timeout (10 seconds unless you set it; it is the time for the whole run), and an answer the hook cannot read. Add --on-error allow to let the edit through instead, with a warning to the agent:
echo '{"session_id":"try-2","tool_name":"Edit","tool_input":{"file_path":"internal/billing/invoice.go"}}' | ./smeldr-cli hook pre-edit --repo acme/app --root "$PWD" --on-error allow
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","additionalContext":"Smeldr: the server at http://localhost:8080 did not answer which Decisions apply to internal/billing/invoice.go: request: Post \"http://localhost:8080/mcp/message\": dial tcp [::1]:8080: connect: connection refused. The edit was let through without the Decisions that apply (--on-error allow)."}}
Start the server again before you go on.
Give the hook a token
In your project, the hook reads the server's address and a token from .smeldr-cli.env in the project's top directory, or from SMELDR_URL and SMELDR_TOKEN. Use a token for the agent, such as the one Connect an agent makes: it needs the Author role and read on the places that matter. Create .smeldr-cli.env with two lines:
SMELDR_URL=http://localhost:8080
SMELDR_TOKEN=<the agent's token>
Keep the file out of version control: add .smeldr-cli.env to the project's .gitignore.
The hook shows only what its token may read. An Area the token may not read is left out, so a file only that Area covers is allowed as if no Area covered it, and a Decision the token may not read is not listed at all. Give the hook's token read on every place whose Decisions the agent should see.
Register the hook in Claude Code
The hook runs as a command, so Claude Code needs the full path of smeldr-cli. In the directory where you ran Getting started, print it:
echo "$PWD/smeldr-cli"
In PowerShell:
"$PWD/smeldr-cli" -replace '\\', '/'
On Windows, Claude Code runs hooks through Git Bash, so the path is written with forward slashes and without .exe, as the PowerShell line prints it. Put the hook in the project's .claude/settings.json, with that path in place of /home/you/smeldr-start/smeldr-cli and your repository in place of acme/app:
{"hooks": {"PreToolUse": [{"matcher": "Edit|Write|MultiEdit|NotebookEdit",
"hooks": [{"type": "command", "command": "/home/you/smeldr-start/smeldr-cli hook pre-edit --repo acme/app", "timeout": 30}]}]}}
The matcher names Claude Code's edit tools. Keep the hook's timeout (seconds, Claude Code's limit) above the hook's own --timeout, so the hook always answers before Claude Code gives up on it.
Then ask Claude Code for a change to internal/billing/invoice.go. Its first edit is denied, it reads the Decisions in the reason, and it makes the edit again if the change keeps to them. In the run for this page, asked to add a field to the invoice type, the agent judged that a new field does not mutate an issued invoice, made the edit again, and said so in its answer. With the server stopped, the edit was held and the file stayed as it was.
What the hook guarantees, and what not
- Claude Code enforces the deny, not the agent. An edit with Claude Code's edit tools to a file an Area covers is held back until the agent has been shown the Decisions in force for that file in this session.
- The hook does not check that the change keeps to the Decisions. That judgment is the agent's, and yours when it asks.
- It does not see files written through the shell, for example a command that appends to a file.
- A subagent's edits go through the hook too, and count under the session that started it.
- An agent that already asked Smeldr itself still gets one deny per file, because the hook cannot see that call.
- A Decision's text that could not be read, or not in time, is marked so on its line, so the agent does not take a title for the whole rule.
- What the hook has shown is kept per session in the user's cache directory (
SMELDR_HOOK_STATE_DIRputs it elsewhere) and removed after seven days.
Ask the agent to plan with it too
The hook acts at the moment of the edit. To have the agent look before it plans, add a passage like this to the project's CLAUDE.md:
Before you change a file, ask Smeldr which Decisions apply to it
(`decisions_for_paths` with this repository and the paths). Read the
Decisions in force, and say in your plan how the change keeps to them. If a
change would go against one, stop and ask instead of editing.
The passage is a rule the agent is told to follow; nothing enforces it. The hook is the part Claude Code enforces. Find the Decisions that apply shows the same question asked by hand.