Open-source security monitoring for small cloud operators. AI-native. Go.
mallcop is a Go-native security monitoring CLI. It ingests your cloud and SaaS
audit events, runs 17 deterministic detectors that look for real-world attack
patterns, then drives each finding through an LLM-backed investigation cascade
(triage → investigate → deep panel → committee consensus) before paging a human.
Findings and decisions are written to a git-native store, so every verdict is
durable, replayable, and auditable. mallcop reads one file — mallcop.yaml —
and needs zero flags for the common path.
One line (Linux & macOS):
curl -fsSL https://mallcop.app/install.sh | shDownloads the prebuilt binary for your platform, verifies its checksum, and
installs it to /usr/local/bin or ~/.local/bin (no sudo).
Go toolchain:
go install github.com/mallcop-app/mallcop/cmd/mallcop@latestRelease binaries:
Pre-built binaries for Linux (amd64/arm64) and macOS (Apple Silicon) are
attached to each GitHub release.
Download the archive for your platform, extract, and place mallcop on your PATH.
1. Install mallcop (see above).
2. Initialize:
mallcop initmallcop init: created <dir>/mallcop.yaml (config — offline inference)
mallcop init: created <dir>/store/ (findings store)
mallcop init: created <dir>/events.jsonl (sample events)
Next steps:
1. Run the scan (reads mallcop.yaml — no flags needed):
mallcop scan
2. Add a source: edit mallcop.yaml -> connectors:
(a github org, or a cloud source like aws/azure)
3. For managed LLM resolution (offline is the fail-safe default):
mallcop init --pro && export MALLCOP_API_KEY=mallcop-sk-...
init writes the one file mallcop reads (mallcop.yaml), a git-backed
store/ for findings + decisions, and a sample events.jsonl so the next
step runs with zero credentials. Re-running init is a no-op — it never
overwrites a file that already exists.
3. Run the scan — no flags:
mallcop scanScan complete
Events scanned: 1
Findings detected: 2
Escalated: 2
Resolved: 0
scan discovers mallcop.yaml, reads its connectors:/store:/inference:
settings, runs the detector fleet, drives each finding through the cascade,
and commits findings + decisions into store/. With no inference endpoint
configured (the OSS default), every finding force-escalates — the documented
fail-safe — so the sample scan above runs end to end with zero credentials.
Exit code 1 means findings were detected, not that the scan failed.
(The old flag-only invocation — mallcop scan --events events.jsonl --store store — still works and takes precedence over the config for any flag you
pass explicitly. Flags exist for scripting; mallcop.yaml is the primary path.)
4. Add a source. Edit mallcop.yaml's connectors: list directly, or use
the config primitive:
mallcop config set connector --kind=github --id=my-org --org=my-org5. (Optional) Enable managed LLM resolution. mallcop init --pro points
mallcop.yaml at the donut rail (Forge-managed inference); set your key and
re-scan:
mallcop init --pro
export MALLCOP_API_KEY=mallcop-sk-...
mallcop scanBYOK/BYOI (your own vendor URL + key) works the same way — set
inference.endpoint/inference.key_env in mallcop.yaml (inference.mode
is a label, e.g. byoi; endpoint+key_env are what the scan actually
resolves), or the MALLCOP_INFERENCE_URL/MALLCOP_API_KEY env pair, which
always wins over the config.
6. Inspect what was recorded:
mallcop status --store storeStore: store
Findings: 2 recorded
Decisions: 2 recorded
State: idle
status's "Decisions" is every resolution ever recorded in the store
(escalations included) — not the same number as scan's per-run "Resolved"
line above, which counts only that run's non-escalate (auto-resolved)
findings. A store can show decisions recorded even when its last scan
resolved none of them itself — both got escalated to a human instead.
mallcop init --create-repo owner/name turns the local scaffold above into a
customer-owned deployment repo, pushed to a real GitHub repo, that scans
on a schedule without you running anything locally:
export MALLCOP_GITHUB_TOKEN=$(gh auth token) # or your own --github-token-env
mallcop init --create-repo my-org/my-mallcop --proWhat gets scaffolded, on top of mallcop.yaml/store//events.jsonl:
go.modpinninggithub.com/mallcop-app/mallcopat a specific release tag (--mallcop-version, default: latest) — a THIN-EMBED dependency pin, never a fork or vendored copy.detectors/— write your own detector here as a Go package (package main, one subdirectory per detector); the scheduled Action builds each one to awasip1/wasmmodule underdetectors/bin/, andmallcop scanloads it exactly like a built-in framework detector viadetectors.sidecars.dir. Seedetectors/README.md(scaffolded into the repo) andexamples/sidecar-detectorin this repo for a worked example.connectors/— write your own connector as a standalone Go binary (same shape as the shipped sibling connectors below) and pointmallcop.yaml'sconnectors:at it withkind: cloud. Seeconnectors/README.md(scaffolded into the repo)..github/workflows/scan.yml— runs on a schedule: installs the pinned prebuiltmallcoprelease binary (never rebuilds the core binary from repo content), builds anydetectors/*to.wasm, runsmallcop scan, and pushes the resultingstore/history back to this same repo on a dedicatedmallcop-findingsbranch (kept offmainby.gitignore) so findings persist across ephemeral runner instances.
The core binary is always the pinned prebuilt release; only your own
detectors/* ever compile from repo content, and only to wasip1/wasm. You
never need a local Go toolchain to run the deployment — only to author a new
detector or connector for it.
learning.autonomy in mallcop.yaml controls how much of mallcop's own
self-extension loop applies automatically, versus waiting for you:
mallcop config set autonomy semi # non | semi | fully| Setting | DATA changes (tuning/mapping widen) | CODE changes (authored detectors/connectors) |
|---|---|---|
non (default) |
wait for human approval | wait for human approval |
semi |
auto-apply on a gate-GREEN clean widen | wait for human approval |
fully |
auto-apply on a gate-GREEN clean widen | auto-apply on a gate-GREEN clean widen |
Every proposal — data or code — still has to pass the same gate first (see
below); the dial only decides what happens to a result that already passed.
Contribute-back to the shared OSS pool is never auto-merged at any
setting — an opt-in PR to the upstream mallcop repo always waits for
human/maintainer review, regardless of where the dial is set.
New detection/connection surface is AI-written Go code, gated, never a
declarative spec interpreted by a generic loader. Both proposal shapes — a
new detector or a data widen (tuning/mapping) — go through the same
validate-proposal gate before anything lands:
(1) guard — static invariant guard: protected paths (the committee,
consensus logic) are untouchable; YAML data may only
WIDEN what's seen, never narrow or add a suppress rule
(2) structural — the change builds; authored code passes the import
allow-list (no net/file/exec in a detector)
(3) exam-detect — base vs. head labeled-exam comparison: no regression,
at least one closed detection gap, no undeclared new
firings on the benign corpus
Run it yourself against any diff:
mallcop validate-proposal --base <ref> --head <ref> --jsonA rejected result never touches the committee or the consensus vote — mallcop only ever widens what the detection committee sees; it can never learn to suppress or auto-approve a real attack pattern.
Every mallcop config set ... primitive (connector, autonomy) is the
same mutation surface a chat interface drives in the hosted product
(mallcop-pro): "watch our Linear activity" or "turn on auto-apply for data
changes" resolves to a propose → approve → apply step that calls the
identical core/config mutation this CLI calls directly. There is no
separate code path for chat versus the command line.
| Command | Purpose |
|---|---|
mallcop scan |
Full agentic scan: connect → detect → cascade → store. Reads mallcop.yaml when present; --store/--events/etc. override or substitute for it. |
mallcop detect |
Offline detection only. Reads events JSONL on stdin, writes findings JSONL on stdout. No inference key. |
mallcop init |
Scaffold mallcop.yaml + a findings store + sample events; --create-repo also scaffolds and pushes a deployment repo. |
mallcop status |
Report findings/decisions recorded in a store. Requires --store. |
mallcop config |
Print the effective scan config resolved from mallcop.yaml + the environment. |
mallcop config set |
Mutate mallcop.yaml (connector, autonomy) via a strict, atomic-write primitive. |
mallcop feedback |
Record an operator decision (approve/dismiss) on a finding so future scans honor it. |
mallcop validate-proposal |
Run the free-tier self-extension gate over a proposal diff (guard + structural + exam-detect). |
mallcop collect |
Mine a scan's store for coverage gaps — the self-extension feedstock. |
mallcop exam-detect |
Grade the offline detect layer against the labeled exam corpus. |
# Zero-flag (reads mallcop.yaml from the current dir or an ancestor)
mallcop scan
# File connector, explicit flags (no config needed)
mallcop scan --events events.jsonl --store store
# GitHub connector (built into the core binary): scan a GitHub org's audit log
export GITHUB_APP_ID=...
export GITHUB_APP_PRIVATE_KEY=... # PEM, or a path to it
export GITHUB_INSTALLATION_ID=...
mallcop scan --connector github --github-org my-org --store storeFlags: --config (explicit mallcop.yaml path), --store, --events
(file path or - for stdin), --connector (file | github),
--github-org, --baseline, --tuning, --learned-mappings,
--max-findings, --workers, --json, --base-url. Precedence is
flag > env > mallcop.yaml > built-in default; an absent config leaves
today's flag-only behavior unchanged.
Exit codes: 0 no findings, 1 findings present, 2 scan failure.
mallcop scan
→ connector — fetch/ingest audit events (file, github, or a
standalone cloud-connector binary)
→ detectors — 17 built-in deterministic detectors + any
configured wasip1/wasm sidecar detectors
→ cascade — triage → investigate → deep panel → committee consensus
→ git store — durable, replayable findings + decisions
The cascade resolves each finding through escalating tiers and ends in a committee consensus vote: on every RESOLVE, the gate re-runs the cascade and any-escalate-wins — a safety-first, asymmetric policy because a missed attack (false negative) is catastrophic while an over-escalation merely pages a human. The 17 built-in detectors are embedded in the binary; authored detectors extend that fleet as sidecars (above) without ever touching the committee/consensus code the guard protects.
The core binary ships two built-in connectors:
- file (default) — reads normalized event JSONL from
--eventsormallcop.yaml'sconnectors:(kind: file). - github — pulls a GitHub org's audit log directly, using
GITHUB_APP_ID/GITHUB_APP_PRIVATE_KEY/GITHUB_INSTALLATION_ID.
Additional connectors ship as standalone binaries in the sibling repo
mallcop-app/mallcop-connectors,
wired into mallcop.yaml as kind: cloud, or piped directly:
# install the AWS connector and name it per mallcop's exec convention
go install github.com/mallcop-app/mallcop-connectors/cmd/aws@latest
mv "$(go env GOPATH)/bin/aws" "$(go env GOPATH)/bin/mallcop-connector-aws"
mallcop-connector-aws --since 2026-07-01T00:00:00Z \
| mallcop scan --events - --store storeCurrently shipped standalone connectors: AWS CloudTrail, Azure Activity Log, GCP Cloud Logging, GitHub Audit Log, M365 Management Activity, Okta System Log.
Not yet ported: container_logs, supabase, vercel.
Your own connector for a source not listed above is a standalone Go binary
you author (same shape as the shipped ones — see the "Deploy as a repo"
section) and wire in via kind: cloud; there is no declarative
connector-spec format — a connector is always real code.
17 deterministic detectors run on every scan (and via mallcop detect):
auth-failure-burst config-drift dependency-tamper
exfil-pattern git-oops injection-probe
log-format-drift malicious-skill new-actor
new-external-access priv-escalation rate-anomaly
secrets-exposure unusual-login unusual-resource-access
unusual-timing volume-anomaly
Content-only detectors (e.g. injection-probe, secrets-exposure, git-oops,
config-drift, dependency-tamper, malicious-skill) fire without any history.
The baseline-dependent detectors (new-actor, priv-escalation, unusual-login,
unusual-timing, volume-anomaly, rate-anomaly, exfil-pattern) use an
optional --baseline JSON file for historical context.
MIT. See LICENSE.