claude code broker pattern for abliterated models
everything here assumes authorized work: systems you own, or systems you have written permission to test, with a scope document you could show a client. if you don't have that, stop here.
why claude code stalls mid-task on authorized security work
you're three hours into an engagement. claude code has mapped the app, read the source, written notes. then you ask it to turn a finding into a working proof of concept and it declines, sometimes in the middle of a session it was happy with a minute earlier. we keep examples of exactly this in the refusal archive, like this mid-session block on an in-scope bug bounty target. it isn't just us: in "open models, open harnesses, open security" (the batch, july 31, 2026), deeplearning.ai describes claude code and codex both refusing a security review of their own open-source project.
the usual fix is "swap the base url and use an uncensored model for everything". that works, but you give up the planner you trust for the 95% of the task that was never a problem. there's a narrower option.
the pattern: planner, broker, worker
- planner: your mainstream agent (claude code, opencode, cline). it reads code, plans, edits files, keeps context.
- worker: a low-refusal model, here GLM 5.3 Flash RR on redrouter. it's an abliterated model (what that means) and gets one self-contained subtask at a time.
- broker: a small script between them. it checks the subtask against a scope file, strips anything the worker shouldn't see, calls the worker, and logs the handoff.
the broker is the point. routing becomes an engineering control you can audit, not a vibe.
write a scope file the broker enforces
keep it boring and machine-readable:
# scope.yaml
engagement_id: ACME-2026-11
authorization: "SOW signed 2026-10-30, ref ACME-PT-114"
window: { start: 2026-11-03, end: 2026-11-14 }
in_scope:
hosts: ["staging.acme.example", "10.20.0.0/24"]
repos: ["acme/web-app"]
out_of_scope:
hosts: ["prod.acme.example", "*.payments.acme.example"]
rules_of_engagement:
- "no denial of service"
- "no data exfiltration beyond proof-of-access"
the broker pulls every hostname and ip out of the subtask and refuses to forward it unless each one is in in_scope: ips by cidr match, domains by suffix match. anything on the out_of_scope list is refused even if it also matches an allow rule, and nothing goes out after window.end. it's crude parsing, and that's fine. it catches the typo that sends a payload at prod. for a worked example of building the scope list in the first place, see recon and attack-surface mapping.
the broker itself
about seventy lines of python. it uses the openai sdk against redrouter's base url, https://redrouter.org/v1, which is the documented setup (quickstart, checked november 4, 2026).
#!/usr/bin/env python3
# broker.py: usage: broker.py "<subtask>" < context.txt
import hashlib, json, os, re, sys, time, ipaddress, yaml
from openai import OpenAI
scope = yaml.safe_load(open("scope.yaml"))
task = sys.argv[1]
context = sys.stdin.read()
text = task + "\n" + context
IP_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
HOST_RE = re.compile(r"\b(?:[a-z0-9-]+\.)+[a-z]{2,}\b", re.I)
FILE_EXT = {"py", "txt", "yaml", "yml", "json", "md", "js", "html", "php", "log"}
def targets(text):
ips = set(IP_RE.findall(text))
hosts = {h.lower() for h in HOST_RE.findall(text)
if h.rsplit(".", 1)[-1].lower() not in FILE_EXT}
return ips, hosts
def domain_match(host, patterns):
for p in patterns:
p = p.lower().lstrip("*.")
if host == p or host.endswith("." + p):
return True
return False
def check_scope(text):
ins = scope["in_scope"]["hosts"]
outs = scope["out_of_scope"]["hosts"]
nets = [ipaddress.ip_network(h, strict=False) for h in ins if re.match(r"^[\d./]+$", h)]
names = [h for h in ins if not re.match(r"^[\d./]+$", h)]
ips, hosts = targets(text)
for ip in ips:
addr = ipaddress.ip_address(ip)
if any(addr in ipaddress.ip_network(o, strict=False) for o in outs if re.match(r"^[\d./]+$", o)):
return f"out-of-scope ip {ip}"
if not any(addr in n for n in nets):
return f"ip {ip} not in in_scope"
for h in hosts:
if domain_match(h, outs):
return f"out-of-scope host {h}"
if not domain_match(h, names):
return f"host {h} not in in_scope"
return None
def strip_secrets(text):
text = re.sub(r"(?i)(api[_-]?key|token|password|secret)\s*[:=]\s*\S+", r"\1=[REDACTED]", text)
return re.sub(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", "[REDACTED KEY]", text, flags=re.S)
if time.strftime("%Y-%m-%d") > str(scope["window"]["end"]):
sys.exit("broker: engagement window closed")
if (bad := check_scope(text)):
sys.exit(f"broker: refused, {bad}")
prompt = strip_secrets(text)
client = OpenAI(base_url="https://redrouter.org/v1", api_key=os.environ["REDROUTER_API_KEY"])
resp = client.chat.completions.create(
model="glm-5.3-flash-rr", max_tokens=4096,
messages=[{"role": "system", "content": f"Authorized engagement {scope['engagement_id']}. Rules: {scope['rules_of_engagement']}"},
{"role": "user", "content": prompt}])
answer = resp.choices[0].message.content
with open("handoffs.jsonl", "a") as log:
log.write(json.dumps({
"ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"engagement_id": scope["engagement_id"],
"model": "glm-5.3-flash-rr",
"prompt_sha256": hashlib.sha256(prompt.encode()).hexdigest(),
"response_sha256": hashlib.sha256(answer.encode()).hexdigest(),
}) + "\n")
print(answer)
claude code: a subagent that calls the broker
claude code subagents are markdown files with yaml frontmatter, saved in a project's .claude/agents/ or in ~/.claude/agents/ for every project. the model field takes an alias (sonnet, opus, haiku, fable), a full model id, or inherit (claude code subagents docs, checked november 4, 2026). it picks a model, not a provider: the subagent still goes through whatever endpoint the session uses, so you can't send one subagent to redrouter while the planner stays on anthropic. instead, give the subagent one job: package the subtask and run the broker.
---
name: redteam-worker
description: Use when the main agent declines an in-scope security subtask. Sends it to the broker.
tools: Bash, Read
---
You hand one self-contained subtask to ./broker.py. Write only the minimal
context it needs to context.txt (no credentials, no hosts outside scope.yaml),
then run: python3 broker.py "<subtask>" < context.txt
Return the broker's output verbatim. If the broker refuses, report why and stop.
the planner stays on anthropic. only the brokered subtask ever reaches redrouter.
if you'd rather run the whole session on redrouter, our docs list the env vars (ANTHROPIC_BASE_URL=https://redrouter.org, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_MODEL) in the security agents setup, and this post walks through it. that's the opposite of a broker, though.
opencode and cline config
same idea: the planner keeps its usual provider and the broker is a shell command it's allowed to run. opencode's own redrouter provider config ("baseURL": "https://redrouter.org/v1" in opencode.json) is in the docs, tested with opencode 1.19.5 on october 29, 2026. for the broker pattern you don't need it. add a short instruction to your project rules ("for declined in-scope security subtasks, run python3 broker.py") and allow that one command. cline works the same way through its custom instructions and command approval.
logging every handoff
the broker writes one json line per call: timestamp, engagement id, model, and sha-256 hashes of the prompt and response. hashes, not plaintext, so the log itself isn't sensitive but you can still prove later what was sent and when. keep the plaintext in your engagement notes if your contract needs it. at report time, handoffs.jsonl is your appendix of which findings used which model.
data handling: turn on zdr before client work
zero data retention is off by default. it's one switch in settings, free on monthly plans. flip it before the first client prompt, not after. what we do and don't keep is spelled out in what redrouter keeps and on the security page.
what the worker should never get
- credentials, session tokens, private keys. the broker redacts common patterns, but don't rely on regex. keep secrets out of
context.txtin the first place. - out-of-scope hosts or repos, even as "background".
- client names and personal data the subtask doesn't need. "a login form at staging host A" is usually enough.
- whole repos. send the function, not the codebase.
failure modes
- scope drift. the planner paraphrases a host ("the payments box") and the regex misses it. keep scope ids in prompts and review the log daily.
- over-routing. the planner learns the broker always answers and stops trying itself. watch how often the subagent fires; if it's every task, tighten its description.
- context loss. the worker only sees what you pass. bad output usually means thin context, not a bad model.
- trusting output. the worker is less cautious, not more correct. verify every poc in your lab before it goes anywhere near the target.
- window expiry. extensions happen. update
scope.yamlfrom the signed paperwork, not from memory.
cost
GLM 5.3 Flash RR is $0.20 per 1M input tokens, $0.08 cached and $0.80 output, with a 1M-token context (docs.redrouter.org/models, november 4, 2026). since the worker only sees short, brokered subtasks, it's a small slice of an engagement's token use. current numbers are always on pricing.