RedRouterRedRouter

Blog

claude code broker pattern for abliterated models

A hand-operated railroad switch with its lamp, overlaid with red pixel blocks and the title The broker pattern for abliterated models
Cover based on a photo by PIVISO, CC0 1.0

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

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

failure modes

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.