Keep your Claude subscription, add a GLM reviewer to every edit
You already pay for Claude, and Claude Code does good work. But the model that wrote a change is the one least likely to see what's wrong with it. A second model, reading the same diff cold, catches a different set of mistakes.
This guide adds exactly that: a small Claude Code hook that sends every code edit to GLM-5.3-Flash on CloudCode.ONE for an independent review. Claude keeps running on your existing subscription — nothing about your setup changes. Only the reviews go through CloudCode.ONE, and a typical one costs a fraction of a cent.
How it works
The hook runs after every Edit, Write or MultiEdit Claude Code makes:
- It takes the change as a diff against
HEAD — or the whole file, if it's new or outside a git repository — and sends it to GLM-5.3-Flash with a strict reviewer prompt.
- If GLM answers
LGTM, nothing happens.
- If GLM finds problems, its review goes straight back to Claude, which has to respond to it. The edit stays; Claude decides what to fix.
- If the review can't run — no key, a timeout, an API error — you see a message, and Claude is told the edit was not reviewed. It never passes silently.
- Only code is reviewed. Markdown, plain text, JSON and notebooks are skipped without an API call, and diffs over 60,000 characters are cut, with the cut reported.
Here's what Claude received after writing a deliberately broken mean():
GLM Flash review of fixture.py:
1. Wrong math: mean divides by len(xs) - 1. That's the sample variance
denominator, not the mean. … Should be len(xs).
2. ZeroDivisionError for single element: mean([5]) → 5 / 0 → caught by
except → silently returns 0. Wrong result.
3. Bare except: catches everything including KeyboardInterrupt, TypeError,
masking real bugs. Should be except ZeroDivisionError: at most.
4. Silent failure: returning 0 for empty input hides errors from callers.
Getting that example took some persuading. Asked plainly for the broken version, Claude wrote a correct one instead — and GLM answered LGTM and stayed quiet, which is the hook working too.
What it costs
| Change reviewed |
Time |
Cost |
| A small edit (a few lines) |
4–11 s |
$0.00003–$0.0002 |
| A whole new 11 KB file |
31 s |
$0.0014 |
Measured on GLM-5.3-Flash, at $0.165 per million input tokens and $0.55 per million output. Two dollars of credit — the smallest top-up — covers over a thousand full-file reviews, or over ten thousand small ones. Claude waits for each review, so expect a few seconds per edit, and up to half a minute on a big new file.
Set it up
Tested on Linux with Claude Code 2.1.304.
1. Install jq, curl and git
sudo apt install -y jq curl git # Debian/Ubuntu
brew install jq # macOS: curl and git ship with the system (untested there)
2. Get a key and check it
Reviews use GLM-5.3-Flash, a paid model, so add a little credit on cloudcode.one and copy your key from the dashboard. Then make one test call:
export CLOUDCODE_API_KEY='sk-...'
curl -s https://api.cloudcode.one/v1/chat/completions \
-H "Authorization: Bearer $CLOUDCODE_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"glm-5.3-flash","reasoning_effort":"low","messages":[{"role":"user","content":"Review: def add(a, b): return a - b"}]}' \
| jq -r '.choices[0].message.content'
You should get a short review pointing out that add subtracts. Keep reasoning_effort: "low": at the default, a 17 KB diff took over 90 seconds and timed out.
3. Save the script
Save this as ~/.claude/hooks/glm-review.sh, then chmod +x ~/.claude/hooks/glm-review.sh:
#!/usr/bin/env bash
set -euo pipefail
# ===== CONFIG (edit these to the values confirmed in step 2) =====
GLM_BASE_URL="https://api.cloudcode.one/v1" # OpenAI-compatible base URL
GLM_MODEL="glm-5.3-flash" # exact model ID from cloudcode.one model list
GLM_REASONING_EFFORT="low" # default reasoning times out on diffs over a few KB
MAX_DIFF_CHARS=60000 # larger diffs are cut (and the cut is reported)
# Only code is sent for review: notes, data and memory files are not. Space-separated, lowercase.
REVIEW_EXTS="py sh bash zsh js mjs cjs ts tsx jsx go rs c h cc cpp hpp java kt rb php sql lua jl swift scala cs"
REVIEW_NAMES="Makefile Dockerfile"
# ==================================================================
input=$(cat)
tool_name=$(printf '%s' "$input" | jq -r '.tool_name // ""')
[[ "$tool_name" =~ ^(Edit|Write|MultiEdit)$ ]] || exit 0
file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // ""')
[ -n "$file" ] && [ -f "$file" ] || exit 0
name=$(basename "$file"); ext="${name##*.}"; [ "$ext" = "$name" ] && ext=""
ext=$(printf '%s' "$ext" | tr '[:upper:]' '[:lower:]')
if [ -n "$ext" ]; then
case " $REVIEW_EXTS " in *" $ext "*) ;; *) exit 0 ;; esac
else
case " $REVIEW_NAMES " in *" $name "*) ;; *) exit 0 ;; esac
fi
# Tell the model (and the user) that this edit was NOT reviewed, instead of passing silently.
skipped() {
jq -n --arg f "$file" --arg why "$1" '{
systemMessage: ("GLM review skipped for " + $f + ": " + $why),
hookSpecificOutput: {hookEventName: "PostToolUse",
additionalContext: ("GLM review did NOT run for " + $f + " (" + $why + "); do not treat this edit as reviewed.")}
}'
exit 0
}
# Diff against HEAD in the file's own repository; untracked files, and files outside any
# repository, are diffed against /dev/null so new code gets reviewed too.
dir=$(dirname "$file")
if git -C "$dir" ls-files --error-unmatch -- "$file" >/dev/null 2>&1 \
&& git -C "$dir" rev-parse --verify -q HEAD >/dev/null 2>&1; then
diff=$(git -C "$dir" diff HEAD -- "$file" 2>/dev/null || true) # staged + unstaged changes
else
diff=$(git diff --no-index -- /dev/null "$file" 2>/dev/null || true) # exit status 1 = files differ
fi
[ -n "$diff" ] || exit 0 # tracked file with no change against HEAD
note=""
if [ "${#diff}" -gt "$MAX_DIFF_CHARS" ]; then
note=" (diff cut from ${#diff} to $MAX_DIFF_CHARS characters; the rest was not reviewed)"
diff="${diff:0:$MAX_DIFF_CHARS}"
fi
[ -n "${CLOUDCODE_API_KEY:-}" ] || skipped "CLOUDCODE_API_KEY is not set"
payload=$(jq -n --arg m "$GLM_MODEL" --arg e "$GLM_REASONING_EFFORT" --arg f "$file" --arg d "$diff" '{
model: $m,
temperature: 0.1,
reasoning_effort: $e,
messages: [
{role: "system", content: "You are a strict senior code reviewer. Find bugs, security issues, and logic errors in this diff. Be concise. If no issues, reply exactly: LGTM"},
{role: "user", content: ("File: " + $f + "\n\nDiff:\n" + $d)}
]
}')
# The key goes in through a pipe (escaped for curl's config syntax) and the payload through
# stdin, so neither shows up in the process list and a large diff cannot hit the
# per-argument size limit.
key=${CLOUDCODE_API_KEY//\\/\\\\}; key=${key//\"/\\\"}
errfile=$(mktemp); trap 'rm -f "$errfile"' EXIT
response=$(printf '%s' "$payload" | curl -sS --max-time 90 "$GLM_BASE_URL/chat/completions" \
--config <(printf 'header = "Authorization: Bearer %s"\n' "$key") \
-H "Content-Type: application/json" \
--data-binary @- 2>"$errfile") \
|| skipped "request failed: $(head -c 200 "$errfile")"
review=$(printf '%s' "$response" | jq -r '(.choices[0].message.content // "") | .[0:3000]' 2>/dev/null || true)
if [ -z "$review" ]; then
err=$(printf '%s' "$response" | jq -r '(.error.message // .error // empty) | tostring' 2>/dev/null | head -c 200 || true)
skipped "no review in the API response${err:+: $err}"
fi
# Block (PostToolUse: feeds the findings back to Claude) unless the model answered LGTM;
# punctuation and markdown around it ("**LGTM**", "LGTM.") still count as LGTM.
verdict=$(printf '%s' "$review" | tr -cd '[:alnum:]' | tr '[:lower:]' '[:upper:]')
if [ "$verdict" != "LGTM" ]; then
jq -n --arg f "$file" --arg r "$review" --arg n "$note" \
'{decision: "block", reason: ("GLM Flash review of " + $f + $n + ":\n" + $r)}'
elif [ -n "$note" ]; then
skipped "only partly reviewed$note"
fi
exit 0
4. Register the hook and the key
Add this to ~/.claude/settings.json to review edits in every project, or to one repository's .claude/settings.json for just that project:
{
"env": {
"CLOUDCODE_API_KEY": "sk-..."
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": "bash ~/.claude/hooks/glm-review.sh", "timeout": 120 }
]
}
]
}
}
If the file already has env or hooks sections, merge into them rather than replacing them. This does the merge — run it once only, because a second run adds the hook twice:
f=~/.claude/settings.json; [ -f "$f" ] || echo '{}' > "$f"; cp "$f" "$f.bak"
jq --arg k "$CLOUDCODE_API_KEY" '.env.CLOUDCODE_API_KEY = $k
| .hooks.PostToolUse += [{"matcher":"Edit|Write|MultiEdit",
"hooks":[{"type":"command","command":"bash ~/.claude/hooks/glm-review.sh","timeout":120}]}]' \
"$f.bak" > "$f"
Three details:
- Don't add
ANTHROPIC_BASE_URL or ANTHROPIC_AUTH_TOKEN. Those would move Claude Code itself onto the gateway. Leave them out and Claude Code stays on your Claude subscription; only the hook talks to CloudCode.ONE.
- Keep
timeout above the script's 90-second curl limit, or Claude Code kills the hook before it can report a timeout. Raise both together if you raise one.
- The key sits in plain text in
settings.json, so never commit that file. Exporting CLOUDCODE_API_KEY in your shell profile instead works just as well.
Restart Claude Code (or open /hooks) and check the hook is listed under PostToolUse.
5. Try it
Ask Claude Code to write a small, deliberately flawed fixture — a mean() that divides by len(xs) - 1, say — and the review should come back right after the write. To test the script on its own, feed it the JSON Claude Code sends:
printf '{"tool_name":"Edit","tool_input":{"file_path":"/abs/path/to/file.py"}}' | bash ~/.claude/hooks/glm-review.sh
Choosing the reviewer
GLM_MODEL at the top of the script picks the reviewer. GLM-5.3-Flash is the default because it's cheap and catches real bugs. mistral-large-4 reviewed the same 11 KB file in 12 seconds instead of 31, at about fourteen times the cost (about $0.02). Either works with the script as it is; the review is labelled "GLM Flash" whichever you pick, so change that line if it bothers you.
Before you install it everywhere
- Privacy. Every reviewed change leaves your machine, including any secrets inside edited code files. For sensitive repositories, register the hook per project instead of user-wide, or leave it out of those projects.
- It's a second opinion, not an oracle. In our own use, roughly half the findings were repeats, already handled, or simply wrong — both reviewers above made at least one confident claim the code contradicts. Let Claude triage each finding: fix the real ones, explain the rest, never apply them blindly.
- The whole diff is reviewed. For a tracked file the hook sends every change since
HEAD, so earlier uncommitted work in that file is reviewed again. Commit often and reviews stay small.
- Cost scales with edits. There's one call per code edit, and many small edits to a large new file resend the whole file each time.
If it doesn't work
| Symptom |
Fix |
GLM review skipped … CLOUDCODE_API_KEY is not set |
Put the key in settings.json env, or export it before starting Claude Code |
GLM review skipped … no review in the API response |
Wrong key or model ID; the text after the colon is the API's own message |
GLM review skipped … timed out |
A big diff or a busy moment: lower MAX_DIFF_CHARS, or raise curl's --max-time and the hook timeout together |
| Nothing happens on edits |
Check /hooks lists it, the file type is in REVIEW_EXTS, and jq is installed; then run the one-file test above |
New to CloudCode.ONE? Top up from $2 and a key is provisioned instantly.