The post reviewer
Reads your post against seven fixed rules and gives PASS or FIX per rule, with one concrete fix. It never rewrites behind your back.
Free skill
Build approval gates, permission rules, a blocking hook and a simple log into your setup, so your AI employees prepare the work and you decide what goes out.
My AI employees write, plan, check and render. They never publish, send or upload. This guide shows how I make that a fact of the setup, not a promise in a prompt.
An AI employee works fast and sounds sure of itself, even when it’s wrong. Most mistakes are cheap: a weak hook, a typo, a caption that’s too long. You fix them in a minute.
Some actions can’t be undone. A post that goes live. An email that reaches your whole list. A file that overwrites your business knowledge. For those, “the AI usually gets it right” is not good enough.
So I split the work in two:
output/ folder, renders files, suggests changes.Everything below follows from that split.
One check at the very end is too late. By then you’ve already paid for a voice-over or spent an hour on visuals for a script that was wrong. So I use two gates.
| Gate 1: text | Gate 2: final | |
|---|---|---|
| When | After writing, before production | After production, before it goes out |
| What you check | Hook, caption, script, slide text | The rendered video, carousel or email as people will see it |
| Why here | Text is cheap to change. Paid or slow production only starts after a yes. | You see what text alone can’t show: timing, layout, readability |
| Outcome | Approved, or back with a reason | Approved, or back with a reason |
The rule: production only starts on approved text. Shipping only happens after approved output.
Before gate 1, I let the post reviewer (BX-004, keyword CHECK) read every post. It checks the post against a fixed list of rules and gives PASS or FIX per rule, with one concrete fix. It never rewrites anything without telling you.
The reviewer is not the gate. It’s the colleague who reads your draft before you look at it. It catches the obvious things, so your own review goes to judgement: is this good, is this me, is this true?
Common mistake: treating an AI PASS as approval. The reviewer can only check rules it knows. You’re the one who knows whether a claim is true.
Your business knowledge (who you are, what you sell, how you write) is the base every employee reads from. If an employee can change it freely, one wrong edit spreads into everything.
So employees never write there directly. They write a proposal: a small file in a proposals/ folder that says:
---
target: brain/voice.md
action: change
submitted-by: social department
date: 2026-10-25
---
## Was
Sentences: short.
## Becomes
Sentences: short. Max 15 words in a spoken hook.
## Why
The reviewer flagged long hooks three weeks in a row.
Example: this proposal is fictional and only shows the format.
You read it, and you apply it or throw it away. Your knowledge only changes when you say so.
Instructions alone are not enough. The Claude Code docs say it directly: CLAUDE.md files are context, not enforced configuration. To block something whatever Claude decides, you need settings or a hook.
So I stack four layers. Each one catches what the one before can miss.
CLAUDE.md and rules files: “You never publish.”.claude/settings.json: deny or ask for specific tools and commands.Claude Code reads permission rules from .claude/settings.json in your project. There are three lists:
| List | What it does |
|---|---|
allow |
Claude can use this without asking you |
ask |
Claude must ask you every time |
deny |
Claude can’t use this at all |
The order matters. Claude Code checks deny first, then ask, then allow. The first match wins, and a more specific rule doesn’t change that order. An allow rule can’t make an exception to a deny rule.
A small example:
{
"permissions": {
"allow": [
"Read",
"Edit",
"Bash(npm run test *)"
],
"ask": [
"Bash(git push *)"
],
"deny": [
"Read(./.env)",
"Bash(python3 scripts/publish.py *)"
]
}
}
What this does:
git push asks you first..env file, where your keys live.A * stands for any text. Bash(git push *) matches every command that starts with git push.
Two things the docs are clear about:
Bash(git push *) stops git push origin main, but not the same thing written another way, like bash -c 'git push origin main'. A deny rule covers the form Claude usually writes. It’s not a wall. That’s why layers 3 and 4 exist.A hook is a small script that Claude Code runs automatically at a fixed moment. A PreToolUse hook runs before a tool is used. It can look at what Claude is about to do and stop it.
This is the layer that reads the full command text with your own logic. It doesn’t depend on Claude remembering a rule.
Save this as .claude/hooks/block-publish.py:
#!/usr/bin/env python3
"""PreToolUse hook: block shell commands that publish or send."""
import json
import re
import sys
BLOCKED = [
(r"PUBLISH_ENABLED\s*=\s*1", "tries to switch the publish lock on"),
(r"scripts/publish\.py", "runs the publish script"),
(r"scripts/send_newsletter\.py", "runs the newsletter send script"),
]
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(0) # can't read the input: no decision, the permission rules still apply
command = data.get("tool_input", {}).get("command", "")
for pattern, reason in BLOCKED:
if re.search(pattern, command, re.IGNORECASE):
print(
f"Blocked by block-publish hook: this command {reason}. "
"Publishing and sending are done by a human. Ask the user.",
file=sys.stderr,
)
sys.exit(2)
sys.exit(0)
Add the hook to .claude/settings.json, next to your permissions:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/block-publish.py\""
}
]
}
]
}
}
PreToolUse is the moment: before a tool runs."matcher": "Bash" means it only runs for the Bash tool, the one that runs shell commands. A matcher like "Edit|Write" would match either of those two tools.$CLAUDE_PROJECT_DIR is the root of your project. Claude Code fills it in, so the path works wherever you start.An exit code is the number a script returns when it ends. Claude Code reads that number to decide what happens next. For a PreToolUse hook:
| Exit code | What happens |
|---|---|
0 |
No objection. The command goes through the normal permission rules. Exit 0 does not approve it. |
2 |
Blocked. The command doesn’t run. What you wrote to stderr (the error output) is sent to Claude as the reason, so it can adjust. |
Anything else, including 1 |
A non-blocking error. The command still runs. You see a short error notice. |
That last row catches people out. In most programs, exit 1 means failure. In a Claude Code hook, exit 1 doesn’t block anything. If your hook is meant to enforce a rule, use exit 2.
Instead of exit 2, a hook can exit 0 and print a JSON object. JSON is a structured text format with keys and values. This gives you more control:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Publishing is done by a human. Ask the user."
}
}
permissionDecision can be:
"deny": cancel the tool call and send the reason to Claude."ask": show you the normal permission prompt."allow": skip the prompt. Deny and ask rules from your settings still apply, so a hook can’t open a door your settings closed.The docs say to choose one approach per hook: exit codes, or exit 0 with JSON. Don’t mix them.
Run the script yourself with a fake input:
echo '{"tool_name":"Bash","tool_input":{"command":"python3 scripts/publish.py --now"}}' \
| python3 .claude/hooks/block-publish.py; echo "exit: $?"
You should see the “Blocked” message and exit: 2. Then try a harmless command like ls and check for exit: 0. Type /hooks in Claude Code to see that the hook is registered.
Common mistake: a hook that crashes. If your script fails with an error, it exits with a code other than 2, and the command runs. Test every hook with a blocked and an allowed example.
Note: my hook exits 0 when it can’t read the input. Then the permission rules decide. If you’d rather block when in doubt, exit 2 there. Then a broken input blocks every shell command until you fix it.
The last layer lives in the script that would actually publish. It checks an environment variable (a setting the operating system passes to every program) before it does anything.
Set the lock in .claude/settings.json. The env key sets variables for every Claude Code session and the commands it runs:
{
"env": {
"PUBLISH_ENABLED": "0"
}
}
And in the publish script:
import os
import sys
if os.environ.get("PUBLISH_ENABLED") != "1":
sys.exit("Publishing is locked. PUBLISH_ENABLED must be 1, set by a human.")
Now the script refuses to run inside Claude Code, even if every other layer failed. When you want to publish, you run the script yourself, in your own terminal, with PUBLISH_ENABLED=1 set on purpose.
Notice how the layers fit together. The settings set the lock to 0. The hook blocks any command that tries to set it to 1. The script refuses unless it’s 1.
Tip: for email, use a double lock. The send function needs
SEND_ENABLED=1and a second flag that says you’re not in test mode. If one is missing, it stops with a clear message instead of failing silently. Better still: let the AI only create drafts in your email tool. Sending is a button you press.
A log is how you know later what happened and why. Keep it simple: one line per event in a text file, in JSON. Log:
One line per decision:
{"ts": "2026-10-25T14:02:00Z", "item": "week-43/post-03", "gate": "text", "from": "pending", "to": "back", "reason": "Hook claims a number without a source", "actor": "robin"}
Example: this log line is fictional.
Never overwrite an earlier line. Only add. Then the log shows the whole history, including the decisions you changed your mind about.
Why bother? Because the reasons you send work back are your best teaching material. After a few weeks you see the same reason three times. That reason becomes a rule in your reviewer, or a proposal for your brain folder.
Check:
- Do your employees write only into
output/andproposals/?- Is there a text gate before anything slow or paid starts?
- Is there a final gate where you see the real output before it goes out?
- Does every change to your business knowledge go through a proposal?
- Is your
.envfile in thedenylist?- Are publish and send commands in
denyorask?- Is there a PreToolUse hook that blocks publish and send commands with exit 2?
- Did you test that hook with one blocked and one allowed command?
- Does the publish or send code itself check a lock variable?
- Is every decision logged with a reason, and never overwritten?
If you can answer yes to all ten, nothing goes live without you.
Read From three skills to a department to see how several employees, their rules and these gates fit into one folder.
Reads your post against seven fixed rules and gives PASS or FIX per rule, with one concrete fix. It never rewrites behind your back.
Free skill