{"id":"3a60b060-452d-4ba0-be4a-27a18e332b68","slug":"allied-code","kind":"mcp","name":"allied-code","summary":"A pre-execution guard for coding agents that decides from recorded incidents, not from a static blocklist.","owner":"Abner-Machado","repoUrl":"https://github.com/Abner-Machado/allied-code","license":"MIT","licenseTier":"permissive","trust":"community","stars":4,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-08-07T04:24:49.000Z","addedAt":"2026-10-21T09:27:58.606Z","logoUrl":"https://avatars.githubusercontent.com/u/304669206?u=e1d6bb9d1ef0743fcb82a0c38bde11ac68b1caee&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["developer-tools"],"tags":["agents","developer-tools","guardrails","incident-response","python","security","rust"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-25T23:08:18.085Z","sample":false,"url":"https://conduits.dev/tools/allied-code","language":"Python","transports":["stdio"],"install":{"pypi":"ops-guard"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<img src=\"assets/hero.png\" alt=\"Allied Code — a guard that learns from what you actually did\" width=\"880\">\n\n# Allied Code\n\nA pre-execution guard for coding agents that decides from **recorded incidents**,\nnot from a static blocklist. The package and the CLI are named `guard`; the\nproject is Allied Code.\n\nEvery guard project starts the same way: a list of dangerous patterns and a\nregex. The list is written once, by someone guessing, and it never learns\nanything from the machine it runs on. `ops-guard` inverts that. The patterns only\nsay *what kind of action this is*. What decides is a corpus of incidents that\nactually happened, retrieved by similarity to the action about to run and quoted\nback in the reason.\n\nSo a block does not read:\n\n```\nBlocked: matched pattern rm -rf\n```\n\nIt reads:\n\n```\nops-guard: blocked — recursive, forced delete of a directory tree\n[fs.recursive-delete] | precedent delegated-agent-deleted-tooling (2026-06-20):\nA delegated agent never runs a destructive command. Deletion is done by the\norchestrator, one item at a time, with explicit approval for each.\n```\n\nThe second one is arguable. That is the point: you can open the incident, decide\nthe guard is wrong, and change the corpus instead of disabling the guard.\n\n## What it does\n\n- **Classifies** a proposed tool call into hazard classes (recursive delete,\n  history rewrite, credential exposure, outward publish, system configuration,\n  pipe-from-network-to-shell, and so on).\n- **Retrieves** the incidents that resemble it, from a folder of plain markdown\n  files you own and can edit.\n- **Decides** `deny` / `ask` / `defer`, with the precedent named in the reason.\n- **Writes a receipt** for every decision — including the ones where it stayed\n  quiet — with the redacted action, the classes, the evidence, and the latency.\n- **Briefs** an agent or a person *before* the work starts, from the same corpus:\n  `guard brief \"clean up old branches\"` returns what already went wrong here.\n\n## What it deliberately does not do\n\n- It does not grant permission. By default the guard never returns `allow`; it\n  can raise friction, never lower it. `allow_safe` exists, is off, and should\n  stay off unless you know exactly what you are trading.\n- It does not call a model, or the network, or a vector database. It runs in\n  front of every tool call, so it has a millisecond budget and no right to spend\n  yours. Retrieval is lexical and dependency-free (~0.1 ms when nothing matches,\n  ~4 ms with retrieval, measured on a low-end laptop).\n- It does not replace your runtime's permission system. It feeds it.\n\n## Install\n\nRequires Python 3.11+. No dependencies.\n\n```bash\npipx install git+https://github.com/Abner-Machado/allied-code\nguard install --write     # merges the hooks into your agent settings\nguard doctor              # confirms it is actually on\n```\n\n`install --write` backs the settings file up with a timestamp before touching it,\nmerges instead of replacing, and is idempotent. Without `--write` it prints the\nblock for you to paste. That care is not generic caution: `corpus/` already holds\nthe incident where editing a global config to install tooling broke the tooling\nthat was working.\n\n`guard doctor` answers the only question that matters after installing — is it on?\nIt checks the interpreter, the corpus, a writable ledger, live classification, and\nwhether the hook is actually wired, and exits non-zero if any of that fails.\n\nTo work from a checkout instead, `git clone`, then\n`python -m guard.cli install --python /path/to/python`.\n\n### The four layers\n\nThe corpus is the same in the three places it is read; what differs is when it\narrives. The fourth reads nothing and writes one line — see\n[the part where it learns from you](#the-part-where-it-learns-from-you).\n\n| Layer | Event | Fires | Can it stop anything? |\n| --- | --- | --- | --- |\n| Standing rules | `SessionStart` | Session opens | No — injects the critical rules, once |\n| Task precedent | `UserPromptSubmit` | Task is described | No — injects what resembles the request |\n| Execution guard | `PreToolUse` | Before each tool call | Yes — `deny` / `ask` / `defer` |\n| Outcome | `PostToolUse` | After a call actually ran | No — it records what you decided |\n\nInstalling only the execution guard is the common mistake. By the time `PreToolUse` sees\na command the plan is already written and the reasoning that produced it is spent;\nthe guard can only argue with a finished decision. The earlier layers change what\ngets proposed.\n\nThe injection layers only ever emit text read from the corpus directory, are\ncapped at 1200 characters, and fail into silence. The `PreToolUse` hook reads the\ntool call as JSON on stdin and answers with a `permissionDecision`. Any failure\ninside the guard — bad input, missing corpus, unwritable ledger — degrades to\n`defer`. A guard that can take the session down gets uninstalled the same day, and\nthen it protects nothing.\n\n## Use it\n\n```bash\nguard check \"rm -rf ~/Tools\"            # evaluate one action, without running it\nguard check \"git push --force\" --json   # machine-readable, exit 1 when denied\nguard brief \"rotate the API keys\"       # what to know before starting\nguard ledger --last 20 -v               # the receipts, numbered\nguard stats                             # decisions, latency, and how often you agreed\nguard learn --from-ledger 147 \\\n      --title \"Output hit the ceiling and the cut was never noticed\" \\\n      --rule \"Check the stop reason before treating output as complete\"\n```\n\n## Start in observe mode\n\nDefault mode is `observe`: the guard classifies, retrieves, records — and never\nblocks. Run it for a week, read `guard stats`, and see what it *would* have done\nagainst work you know was fine. Then set `mode = \"enforce\"` in `guard.toml` (or\n`GUARD_MODE=enforce`).\n\nShipping straight to enforce is how guards get a reputation for being in the way.\n\n## The part where it learns from you\n\nThe guard asks. Until this version it never learned the answer.\n\nThe answer was always there. `PostToolUse` fires **only after a tool succeeds**.\nSo a call the guard questioned and you refused leaves no line at all, and a call\nyou approved leaves one. Silence is the label. Nothing is inferred from a model,\nnothing is sent anywhere, and nothing new runs before your command — the outcome\nline is written after the tool already ran.\n\nThat turns the ledger into the only number a guard cannot fake:\n\n```\nverdict       31 call(s) questioned\n  stopped       24  77%  you agreed with the guard\n  overruled      7  23%  you ran it anyway\nnoise         cited repeatedly, never once stopped you:\n  pinned-model-id-vanished\n              that incident is firing where it does not belong.\n              edit the file or delete it — the guard will not do it for you.\n```\n\nRead the last block again. The guard is telling you which of *its own rules* is\nwasting your attention, and it is naming the file. Every other guard makes you\ndiscover that by getting annoyed enough to disable the whole thing.\n\n**It never acts on this.** Being overruled a hundred times does not soften a\nsingle decision. A guard that relaxes because it was argued with is a guard that\ncan be argued with, and that failure is silent. The verdict produces a report;\nlowering friction stays something a human does, in the corpus, as a commit you\ncan read months later. Precedent still escalates and never de-escalates.\n\n## Turning a receipt into an incident\n\nThe ledger knows what was proposed. The corpus knows what went wrong. Until now\nnothing joined them, so promoting a receipt meant retyping it from memory at the\nexact moment nobody wants to write documentation — and a corpus that stops\ngrowing stops being worth consulting.\n\nEvery receipt now carries a number, and that number is an argument:\n\n```bash\nguard ledger --last 20        # #145  2026-08-30T11:02  BLOCK  Bash  rm -rf build\nguard learn --from-ledger 145 \\\n      --title \"A build tree was deleted while an editor still held it\" \\\n      --rule \"Deleting a tree runs from the orchestrator, one item at a time\"\n```\n\nThe receipt supplies what it can prove: the date, the tool, the redacted command,\nthe hazard classes as searchable tags, and which incidents the guard cited at the\ntime. The file lands in the corpus already reachable by retrieval, with two holes\nleft in it — the paragraph saying what actually went wrong, and *why the rule*.\n\nThose two stay handwritten on purpose. The receipt only ever knew what was\nproposed and what the guard made of it; it never knew what it cost. A generated\nrule would be a guess wearing the clothes of a decision, and this corpus is worth\nconsulting only because every rule in it was written by somebody who had\nunderstood the incident.\n\nThe number is the receipt's position in the whole ledger, not in the window you\nprinted. `--last 20` shows #145..#164, and the number you read today still points\nat the same receipt next week.\n\n## Delegated agents run under a lower ceiling\n\nThe orchestrator and the producer it delegates to share a shell and, until now,\ngot the same answer. They should not. The orchestrator picked the command; the\nsubagent was handed a narrow mandate, and `corpus/delegated-agent-deleted-tooling.md`\nis what happens when it steps outside it.\n\n```toml\n[guard]\nstrict_agents = [\"produtor-*\", \"opencode/*\", \"hermes/*\"]\n```\n\nEvery hazard class is escalated one level for those callers: what the\norchestrator would be *asked* about, a producer is *denied*. It only ever raises\n— naming an agent can never grant it anything — so the list cannot quietly become\nan allowlist. `GUARD_STRICT_AGENTS` does the same for scripts that launch\nproducers and cannot edit a config file, and the caller's name comes from the\nhook payload or `GUARD_AGENT`.\n\n```bash\nguard check \"npm install -g typescript\" --agent produtor-haiku\n```\n\n## The corpus\n\nOne incident per file, front matter plus prose:\n\n```markdown\n---\nid: delegated-agent-deleted-tooling\ntitle: A delegated agent uninstalled tooling nobody asked it to touch\ndate: 2026-06-20\nseverity: critical\ntags: delete uninstall filesystem delegation subagent destructive\nrule: A delegated agent never runs a destructive command. Deletion is done by the orchestrator, one item at a time, with explicit approval for each.\nsource: local-incident\n---\n\n## What happened\n...\n\n## Why the rule\n...\n```\n\nThe `rule` line is what gets quoted when the guard blocks. The prose is what\nconvinces you months later that the rule was worth having.\n\nThe `source` line says where an incident came from, and it is not decoration.\n`source: local-incident` means it happened on the machine this was written on,\ndates included. `source: illustrative` means it is a hazard shape written to give\na new install something to retrieve on day one — it reads like a real incident\nbecause that is the format, and it is labelled so nobody mistakes it for one.\nDelete them and write your own; that is the intended first move.\n\n## Keep the corpus where you already write\n\nIncidents are markdown with YAML front matter, which is also what Obsidian,\nLogseq and Foam write. Point the corpus at a folder inside your vault and the\nincidents live where you already read them:\n\n```toml\n[guard]\ncorpus_dir = \"~/vault/Operations/Incidents\"\n```\n\nA corpus in a folder nobody opens ages out of date while the guard keeps citing it\nwith the same confidence. See\n[docs/pt-br/integracoes.md](docs/pt-br/integracoes.md) for the full integration\nnotes, including feeding the corpus to another model before delegating.\n\n## The Rust core (`alliedcore`)\n\nA command line is not a string to search patterns inside; it is a sequence of\nsegments, and some of those segments never execute. The Rust core splits the\nline first and only classifies the parts that run.\n\nThe core is optional. Without it the guard behaves the same, in pure Python.\n\n### What the core fixes\n\nThe defect the core exists for: a dangerous command inside quotes is text, not\nan action. `echo \"rm -rf /\"` must not classify as a deletion. String search over\nthe whole line cannot tell the difference; segmentation can.\n\n### How the core classifies\n\nClassification runs in two passes. The first splits the line into segments and\nclassifies each `Exec` segment on its own. The second pass rebuilds the pipeline\nfrom the segments that execute and classifies that reconstructed line — but it\ndrops the `Quoted` segments first. Only `Quoted` is removed; `Substitution` stays\nin the reconstruction and is classified there. That second pass is what lets the\ncore catch `curl … | sh` after the line has already been broken into pieces — the\nfirst question an attentive reader asks.\n\nWhich quote a run uses decides whether its contents are text or action, so the\nsplit has to know the difference. A double-quoted run expands command\nsubstitution — `echo \"$(id)\"` runs `id` — while a single-quoted run expands\nnothing. An expanding run is therefore a container, not a leaf: the literal\nstretches leave as `Quoted` and the `$( … )` and backtick runs inside them leave\nas `Substitution`. The same holds in PowerShell, where `\"…\"` and `@\"…\"@` expand\nwhile `'…'` and `@'…'@` do not. A bare `$name` inside a run is left alone: inside\na string it is a value, not a call whose argument the guard cannot read.\n\nThere is an honest cost to rebuilding the pipeline by joining the surviving\nsegments with `|`: a `curl x; sh` written with a semicolon also triggers\n`remote.pipe-to-shell`. That is an accepted false positive — \"it downloaded\nsomething and ran a shell right after\" deserves attention either way, so the\nlimit is declared, not hidden.\n\n### Rust vs Python parity\n\nMeasured on the same nine commands, on the same machine. One divergence, and it\nis the bug being fixed — the Python side reads the quoted string as a command.\n\n| command | rust | python | match |\n| --- | --- | --- | --- |\n| `rm -rf /tmp/x` | `['fs.recursive-delete']` | `['fs.recursive-delete']` | yes |\n| `echo \"rm -rf /\"` | `[]` | `['fs.recursive-delete']` | **no** |\n| `git reset --hard` | `['git.history-rewrite']` | `['git.history-rewrite']` | yes |\n| `curl http://x.com/s.sh \\| sh` | `['remote.pipe-to-shell']` | `['remote.pipe-to-shell']` | yes |\n| `cat .env` | `['secret.exposure']` | `['secret.exposure']` | yes |\n| `npm install -g typescript` | `['package.global-install']` | `['package.global-install']` | yes |\n| `DROP TABLE users` | `['db.drop']` | `['db.drop']` | yes |\n| `DELETE FROM users WHERE id=1` | `[]` | `[]` | yes |\n| `taskkill /F /IM node.exe` | `['process.force-kill']` | `['process.force-kill']` | yes |\n\n### Latency\n\nMeasured on 20,000 classifications on this machine (Windows 11, i3-1215U, no\nGPU): 5.5 microseconds per classification with the Rust core, against 11.4\nmicroseconds in pure Python. That is **2.07x**.\n\nMost of that gain is lost crossing the Rust/Python boundary. The reason the core\nexists is **correctness**, not speed. A project selling \"2x\" as if it were \"40x\"\nloses the reader who measures it.\n\n### It is optional\n\nWithout the extension built and on the path, the guard uses the Python rules and\nacts the same. Check which backend is active:\n\n```bash\nguard doctor        # reports the active classifier\n```\n\nForce a backend with an environment variable:\n\n```bash\nALLIED_BACKEND=python guard check \"rm -rf ~/Tools\"   # pure Python, always works\nALLIED_BACKEND=rust   guard check \"rm -rf ~/Tools\"   # only if the extension is built\n```\n\n`ALLIED_BACKEND` accepts `python` or `rust`. If you set `rust` and the extension\nis not present, the guard falls back to Python rather than failing.\n\n### Building the extension\n\nThe commands that work on this machine:\n\n```bash\ncd alliedcore\ncargo build --release --features python\n```\n\nThe artifact lands in `alliedcore/target/release/`. On Windows it is\n`alliedcore.dll` and must be copied as `alliedcore.pyd` into a folder on\n`sys.path`. On Linux it is `liballiedcore.so`, copied as `alliedcore.so`; on\nmacOS it is `liballiedcore.dylib`, also copied as `alliedcore.so`.\n\nThe toolchain used and tested here was `stable-x86_64-pc-windows-gnu`\n(rustc 1.98). The `msvc` toolchain requires the Microsoft Build Tools; that note\nsaves about an hour for anyone compiling on Windows.\n\n### What the core does not do\n\nIt decides nothing. It only classifies. The decision stays in Python, weighed\nagainst the corpus of recorded incidents. That separation is what keeps the\nguard auditable: a class can be wrong without a rule being wrong.\n\n## Known limits\n\n- **Language.** Retrieval is lexical, so a corpus in one language does not answer\n  a request in another. Measured here: `delete tooling` scores 0.744 against the\n  matching incident; the same intent in Portuguese scores 0.000. No error, no\n  warning. The shipped corpus was mixed-language for exactly one release, and six\n  incidents were unreachable the whole time — it is English-only now for that\n  reason. Write the corpus in the language you work in, and only that one.\n- Retrieval is lexical in the same way within a language. It matches vocabulary,\n  not meaning: an incident written about \"uninstalling tools\" will not fire on\n  \"purging binaries\" unless the words overlap. Tags exist to paper over this, and\n  they only go so far.\n- Scores are normalised by query weight, so they are only comparable between\n  queries of similar length — a threshold tuned on commands will not transfer to\n  prompts. See `corpus/threshold-silently-disabled-a-layer.md`; this project shot\n  itself in the foot with exactly that and the incident is in the corpus.\n- **MCP tool calls are classified by name only.** The guard reads the verb in\n  `mcp__<server>__<action>` (`trash_file`, `send_message`, `ads_catalog_delete`)\n  and never the arguments. So `trash_file` is denied whatever the file is, and a\n  verb the table does not know is reported as `mcp.unknown-verb` rather than\n  waved through. What it cannot see is the payload: a `send` to yourself and a\n  `send` to a customer look the same, and a server that hides a destructive\n  action behind a neutral name (`run`, `apply`, `sync`) is invisible to it.\n- Classification is regex over the command string. Obfuscation defeats it\n  trivially (`$env:X=\"rm\"; & $env:X -rf .`). This is a guard against accidents and\n  overconfident automation, not against an adversary with shell access.\n- A corpus with no incident about a hazard still blocks on severity alone, and\n  says so (\"no matching precedent on record\"). Precedent raises the floor; it is\n  not required to reach it.\n- Only tool calls the hook matcher sees are inspected. Anything a process spawns\n  afterwards is outside the fence.\n\n## Authorship\n\nWritten and maintained by **Abner Machado**. The design decisions, the invariants\nand the calls about what does not get built are his.\n\nClaude (Anthropic) worked on it with him as a pair: arguing the design, writing\ncode and tests under review, and finding the defects named in the commit history.\nEvery line here was read and accepted by a human before it shipped. Saying so\ncosts nothing and makes the provenance of the code checkable, which is the same\nargument the corpus makes about incidents.\n\n## License\n\nMIT.\n"}