Mallcop closes its own detection gaps. It watches what fell through, proposes a fix, and lets that fix in only after it passes a gate — safe config changes by default, with a code escape hatch that always ends in a human review. Every demo on this page is a real command with real output. Run them yourself.
The security vendors' moat is "connect to anything, learn to detect anything." Mallcop's answer is a loop the product drives at runtime instead of a batch job of hand-written rules. Every scan that force-escalates an action it couldn't map, or misses a finding it should have caught, becomes feedstock. Mallcop mines that feedstock, proposes a narrowly-scoped fix, and puts the fix through the same gate every time.
The gate is the whole point. It is a $0, no-inference check on a real git diff: it freezes the parts of mallcop the loop must never touch (the committee, the pipeline, the tools, the gate itself), confirms the change still builds, and re-runs the labeled detection exam to prove the proposal closes a real gap without breaking anything that already worked. Only inference — authoring the proposal — costs anything; the gate is free.
There are exactly two things a proposal can be, and they carry very different risk. Mallcop keeps them separate on purpose.
| Data lane safe by default | Code lane gated escape hatch | |
|---|---|---|
| What it changes | A YAML overlay — a widen-only tuning knob or an action mapping | A brand-new Go detector, authored in a worktree jail |
| What it can express | Only widen what the committee sees. Narrowing is inexpressible. | New detection logic keyed on an event type |
| Needs inference? | The gate does not. Only the optional proposer does. | Yes — it writes code |
| How it lands | Applied as your local overlay after the gate passes | Never auto-merged. Emits a human-review artifact. |
| Blast radius | Config only — more alerts, never fewer | Fenced: capped subkey, sandbox, four-layer gate, then a human |
The data lane can only ever make mallcop more watchful — it widens what the committee reviews, so the worst case is a false positive you rule away. It never adds a rule that suppresses or force-resolves a finding. The code lane can do more, so it is fenced in more: it runs on a capped key in a sandbox, clears a four-layer gate, and still only produces a patch for a human to read and merge. The engine never pushes.
The gate and the offline demos on this page are pure OSS — free, deterministic, and reproducible with the mallcop binary. Only the proposer and the code-authoring lane spend anything, and you choose how they are billed.
| Rail | What it costs | Spend cap | Who it's for |
|---|---|---|---|
| Free OSS data lane | $0 — the gate needs no inference | n/a | Anyone. No account, no key. |
| Donut rail | Metered to your mallcop tenant balance | Yes — a dedicated, spend-capped self-ext account | Pro tenants who want managed inference |
| BYOI rail | Metered to your endpoint + key | None — your key, your blast radius | Anyone with their own inference account |
The free data-lane gate and every demo below run today with the OSS mallcop
binary (exam-detect, collect, validate-proposal). The
metered proposer and code-authoring lanes are operator-run through a separate binary,
mallcop-ops selfext (--propose for the data lane,
--run for the code lane): they need donut balance (managed inference) or your own
inference key (BYOI), and they refuse loudly rather than spend without credentials.
Here is "gets smarter over time" made concrete — no inference, no mocks, $0. Mallcop
ships with a labeled detection exam. mallcop exam-detect grades the offline
detect layer against it. Two cases start RED (excerpt below: 7 of the corpus's 17 labeled
scenarios, the ones relevant to this walkthrough):
$ MALLCOP_REPO_ROOT=~/projects/mallcop mallcop exam-detect PASS AC-01-external-access-stolen-cred must_fire: new-external-access emitted: new-external-access PASS VA-01-deploy-burst must_not_fire: volume-anomaly emitted: (none) PASS VA-02-month-end-batch must_not_fire: volume-anomaly emitted: (none) FAIL VA-03-data-exfil must_fire: volume-anomaly emitted: (none) PASS VA-05-quarterly-report-burst must_not_fire: volume-anomaly emitted: (none) FAIL PE-08-aws-poweruser-grant must_fire: priv-escalation emitted: (none) PASS PE-09-aws-readonly-grant-benign must_not_fire: priv-escalation emitted: new-actor exam-detect: 17 labeled (13 passed, 4 failed), 41 unlabeled (skipped) # exit 1
PE-08 is a false negative you can close with pure data. AWS's managed
PowerUserAccess policy grants nearly everything, but its name carries no built-in
elevation keyword (admin / owner / write), so priv-escalation missed the grant.
The fix is a hand-authored, widen-only overlay — the entire file:
# detectors/tuning.yaml — widen-only. Narrowing is inexpressible here. priv_escalation: extra_elevated_keywords: - poweruser
Re-grade with the overlay applied. PE-08 flips to GREEN —
priv-escalation now fires on the grant — and the benign twin
PE-09 (the read-only grant that must not fire) stays GREEN. Widen-only,
no over-fire:
$ MALLCOP_REPO_ROOT=~/projects/mallcop mallcop exam-detect \ --tuning ~/projects/mallcop/detectors/tuning.yaml ... PASS PE-08-aws-poweruser-grant must_fire: priv-escalation emitted: priv-escalation PASS PE-09-aws-readonly-grant-benign must_not_fire: priv-escalation emitted: new-actor FAIL VA-03-data-exfil must_fire: volume-anomaly emitted: (none) exam-detect: 17 labeled (14 passed, 3 failed), 41 unlabeled (skipped) # exit 1 — VA-03 is still open
13 passed → 14 passed, as pure data. No code changed, no inference ran.
VA-03 stays RED on purpose: a data-exfil case needs detection logic that doesn't
exist yet — a tuning knob can't invent a detector. That gap is exactly what the code
lane is for, and why the code lane exists behind a gate and a human review rather than a knob.
Where do proposals come from? From the scan store. When a scan meets an action it can't map,
the offline fail-safe force-escalates it rather than dropping it, and records it. Run a scan
over three events whose actions land in the connector's github_other bucket
(the fixture is test/docdemo/fixtures/gap-demo-events.jsonl in this repo —
this exact command and its output are doc-tested against it). The flags below point at that
fixed fixture so the numbers are exactly reproducible; day to day, the same scan reads
mallcop.yaml with zero flags — see
Configuration:
$ mallcop scan --connector file --events events.jsonl --store ./store --json { "events_scanned": 3, "findings_detected": 2, "escalated": 2, "resolved": 0 } # exit 1 — scan exits non-zero when it has findings (alert semantics)
Then mallcop collect mines the store and surfaces the mapping gap the proposer
would turn into an add-only widen — two repository.transfer events with no
vocabulary mapping, plus the known event types it could map them onto:
$ mallcop collect --store ./store --json { "schema_version": 1, "mapping_gaps": [ { "source": "github", "raw_action": "repository.transfer", "count": 2, "sample_event_ids": ["evt-gap-001", "evt-gap-002"], "suggested_vocabulary": [ /* 64 known event types the proposer maps onto */ ] }, { "source": "github", "raw_action": "environment.protection_rule.override", "count": 1, "sample_event_ids": ["evt-gap-003"], "suggested_vocabulary": [ /* ... */ ] } ], "gap_candidates": [] } # exit 0
That envelope is the proposer's input. On the donut or BYOI rail, one metered inference call turns it into a strict, add-only mapping proposal — which then goes through the same gate as everything else. The feedstock is produced entirely offline and for free.
The gate is a static guard over a real git diff — no inference, no corpus, $0. Its most
important job is refusing to let the loop edit the parts of mallcop that make it trustworthy.
Point it at a diff that touches core/agent/ — the committee and its knobs
— and it rejects, flat:
$ mallcop validate-proposal --base <base> --head <head> --guard-only --json { "schema_version": 1, "tier": "free", "passed": false, "stages": [ { "name": "guard", "passed": false, "evidence": "static invariant guard over ee09ee11..cc4cdeba: 1 finding(s)", "findings": [ { "path": "core/agent/consensus.go", "rule": "protected-path", "detail": "modification on a protected path: the self-extension loop may not touch the committee, pipeline, lint, tools, grader, inference, contracts, cmd, CI, module files, or the guard itself" } ] } ] } # exit 1 — the loop can never edit the committee
A clean, additive change the guard has no opinion on — here a docs edit — passes:
$ mallcop validate-proposal --base <base> --head <head> --guard-only --json { "schema_version": 1, "tier": "free", "passed": true, "stages": [ { "name": "guard", "passed": true, "findings": [] } ] } # exit 0
This is the load-bearing invariant: the loop can widen what the committee sees, but it can never edit the committee. The gate enforces it as diff analysis, not as a prompt the model could talk its way around.
The data lane runs inline in the CLI — a metered proposer call, the offline gate, and a
local overlay, no infrastructure. The code lane is different: authoring a
brand-new Go detector needs a real sandbox and a real human-review boundary, so it ships as a
GitHub Actions workflow that runs in your own fork of
mallcop-app/mallcop. The ephemeral runner is the sandbox; the required
merge check is the gate; the pull request is the human review. The engine never pushes and
never merges.
One operator command scaffolds the workflow into your fork checkout. It writes three files —
a thin caller workflow, a pinned reusable workflow (all the orchestration), and a
CODEOWNERS belt that freezes the committee, grader, and gate paths — then prints
a checklist for the steps GitHub won't let a scaffold do for you (secrets, branch protection):
$ mallcop-ops selfext --scaffold-gha --out ./mallcop-fork wrote 5 CODE-lane file(s) into ./mallcop-fork: .github/workflows/mallcop-selfext-code.yml .github/workflows/selfext-code-reusable.yml .github/workflows/mallcop-version-bump.yml .github/CODEOWNERS .github/MALLCOP_SELFEXT_SETUP.md Next steps (scaffolding cannot set secrets or branch protection for you): 1. Set the inference secret — your OWN donut key, NOT a Forge admin key: gh secret set MALLCOP_SK # paste your mallcop-sk-... key 2. Edit .github/CODEOWNERS: replace @operator with your GitHub handle. 3. Pin the reusable-workflow ref to a mallcop-app/selfext release commit SHA. 4. Require the exam gate + code-owner review before merge to main. 5. Trigger an authoring run: gh workflow run mallcop-selfext-code.yml \ -f detector_id=authored-deploy-burst -f event_type=github.deployment
Once configured, you dispatch a run by naming the gap — the detector id and the connector
event type it keys on. Inside the pinned runner image
(ghcr.io/3dl-dev/selfext-runner), opencode authors the detector on your
secret's inference (your mallcop-sk-* donut key, or a BYOI provider key), the
in-runner four-layer gate runs, and on GREEN a review PR opens in your fork under
your own identity. The runner has contents: write + pull-requests: write
scoped to the fork and nothing else — it cannot write main (branch
protection), cannot reach the upstream repo, and never holds an admin key. You review and merge
the PR; it takes effect only once merged and released.
A merged detector is yours — a private overlay in your own repo. Sharing one back into the
open-source corpus is a separate, opt-in step that needs two things you set once. First,
consent: mallcop config set contribute_back on, which writes
learning.contribute_back: true — the live consent knob. (There is a
similarly-named sovereignty.contribute_back field that gates nothing; the
config set command and the config summary both point you at the live
one.) Second, a credential of your own: because the upstream PR is opened under
your identity and never mallcop's, you set an OSS_CONTRIB_TOKEN secret —
a least-privilege token you control. Miss it and the run tells you exactly how to add it, in the
run summary, instead of failing silently.
# 1. Opt in (writes learning.contribute_back: true — the live knob): $ mallcop config set contribute_back on # 2. Give it a token YOU control (mallcop holds none). Fork the OSS repo, then mint a # fine-grained PAT scoped to ONLY that fork — Contents: read/write + Pull requests: # read/write (or a classic token with just public_repo). Least privilege, nothing else: $ gh secret set OSS_CONTRIB_TOKEN # paste your least-privilege token # 3. Propose an ALREADY-MERGED detector upstream (opens a REVIEW PR — never merges): $ gh workflow run mallcop-selfext-code.yml -f promote_detector=authored-deploy-burst
The upstream PR is a review PR: the OSS repo's own exam gate and code-owner review decide whether it merges, at every autonomy setting. mallcop never merges it for you, and never holds a credential that could.
| Boundary | What enforces it |
|---|---|
| The runner can't merge its own work | Branch protection + required exam check + code-owner review — all outside the job's reach |
| The runner can't touch the committee or gate | In-runner guard stage rejects any edit to core/agent, eval, selfgate, or .github/; a CODEOWNERS belt backs it up |
| No standing credentials leak | Ephemeral VM destroyed after the job; the only secret is your own inference key, redacted from logs |
| Supply chain is pinned | Every uses: is a 40-hex commit SHA; the runner image is pinned by digest; opencode is baked at a checksum-verified version |
Availability. The --scaffold-gha command and the templates ship
today, and the supply chain is now SHA-locked end to end: the scaffold emits a
caller pinned to the central reusable workflow (mallcop-app/selfext), which in turn
pins the runner image (ghcr.io/3dl-dev/selfext-runner) by @sha256 digest
— the image is built in CI from the same mallcop-ops release that authored the
detector above. The single remaining step to enable external forks is flipping that runner
image to public in the registry; until then, run the code lane operator-side (proven above) on
your own inference or the donut rail. Donut billing for the rails above (see
Pricing) is already live. The full as-shipped record, including the
honest egress story on hosted runners, is in the
GHA self-extension runtime design note.
The hosted GitHub-Actions flow above is still being turned on — but the loop it runs is not theoretical. Driven operator-side today (your own inference, or the donut rail), the code lane has already authored a brand-new detector end to end: opencode on GLM-5.2 wrote it in a sandbox jail, and it cleared the full gate.
opencode invocation complete authored_files=8 PROPOSED GREEN gate — reviewable artifact (proposal.patch + gate.json + provenance) guard PASS static invariant guard: 0 findings structural PASS go build OK · go vet OK · import allow-list clean · shape gate clean exam-detect PASS coverage +1 — must-fire fires, benign-twin stays silent # applied=false — the engine never auto-merges
What it wrote is real Go — its own package, a literal Name(), and a pure
Detect keyed on the event type, plus a must-fire and a benign-twin exam scenario
proving it fires when it should and stays silent when it shouldn't. A human reviewed the artifact
and opened it as a pull request against the mallcop repo —
#157: authored by
the product, gate-passed with coverage +1, opened for human review, never auto-merged. That
is the whole loop, end to end, on a real detector.
Self-modifying security tooling is only worth having if you can trust the modifications. Every claim below is enforced in code and provable offline, not a promise.
validate-proposal gate: protected paths frozen, still builds, no regression on
the labeled exam, at least one real gap closed. The gate is free and runs with no inference.Want to see the whole story end to end? The consensus cascade is how a single scan reasons and votes; the security model covers the airlock that sanitizes every byte the AI reads; and the detector catalog lists what mallcop already watches for out of the box.