Skip to content

DefenseClaw

ShadowClaw's detections become readable in the Grafana that ships with DefenseClaw's bundled local observability stack, alongside DefenseClaw's own telemetry.

The two tools answer different questions. ShadowClaw detects AI use from network egress and process behaviour. DefenseClaw discovers AI components that are installed. Disagreement is the interesting signal: egress to a provider with no corresponding installed component suggests an agent that arrived some other way.

Nothing inside DefenseClaw is modified

Two facts make that possible.

  1. DefenseClaw's bundled Collector accepts OTLP on 127.0.0.1:4318 and forwards every log it receives to Loki. Its logs pipeline does not filter by sender, so ShadowClaw exports to it exactly as it would to any collector. That is using an open local port as designed, not altering the product.
  2. The dashboard is installed through Grafana's HTTP API, which writes to Grafana's own database. It is deliberately not dropped into the stack's provisioning directory — on an installed wheel that resolves to <site-packages>/defenseclaw/_data/local_observability_stack, so writing there would mean editing another product's installation, and would be lost on the next upgrade anyway.

No DefenseClaw code, no Cisco AI Defense code, and no files inside either product's installation are changed. --remove reverses the install completely.

Prerequisites

defenseclaw setup local-observability up
defenseclaw setup local-observability status
  • Docker must be running. That stack is a Docker Compose project.
  • The subcommand is local-observability; the TUI labels it Setup: Local Otel.

Run ./scripts/install.sh --check from the ShadowClaw repo root first. It documents the Full Disk Access / Python-binary mismatch, root versus unprivileged coverage, and the three OTLP export paths.

Docker is only needed for their bundled stack

If all you want is ShadowClaw's own findings in Grafana, skip it — Loki 3.x accepts OTLP natively, so the sensor can post straight to a standalone binary. See Grafana and Loki.

Setup

# 1. Run the sensor with the DefenseClaw profile
sudo python3 -m shadowclaw --config config/shadowclaw.defenseclaw.json --dns-sniffer

# 2. Preflight the dashboard install
python3 integrations/defenseclaw/install_dashboard.py --check

# 3. Install it
python3 integrations/defenseclaw/install_dashboard.py

sudo and --dns-sniffer are worth it here. Without the sniffer, provider names come from catalog matching and reverse DNS, and are scored at reduced confidence accordingly.

The installer auto-detects the Loki datasource, creates a ShadowClaw folder, and prints a link. Re-running is idempotent.

Installer flags

Flag Effect
--grafana-url URL Target a Grafana other than the bundled one.
--folder NAME Folder to file the dashboard under.
--dashboard PATH Use a different dashboard JSON.
--datasource-uid UID Pin the datasource instead of auto-detecting.
--token TOKEN Grafana API credential.
--allow-remote Permit a non-loopback Grafana.
--check Preflight only.
--dry-run Print what would change.
--remove Reverse the install.

Why nothing needs configuring on their side

From _data/local_observability_stack/otel-collector/config.yaml:

  • The OTLP receiver listens on 0.0.0.0:4318 for HTTP, with CORS allowing http://127.0.0.1:*, and no authentication.
  • The logs pipeline is receivers: [otlp] → processors: [resource, batch] → exporters: [otlphttp/loki, debug], and does not filter by sender.

So pointing ShadowClaw at http://127.0.0.1:4318 is sufficient. There is no allowlist to add and no file to edit.

One detail decides whose name the findings carry:

resource:
  attributes:
    - key: service.namespace
      value: defenseclaw
      action: insert

insert only fills a gap. ShadowClaw sets service.namespace=shadowclaw on its own resource precisely so this cannot relabel our detections as theirs.

Their debug exporter runs at verbosity: detailed, so this is the fastest way to confirm arrival:

defenseclaw setup local-observability logs --service otel-collector

Ports: no conflict, but do not run two collectors

DefenseClaw's collector binds 127.0.0.1:4317 and 127.0.0.1:4318 — the same ports ShadowClaw's own collector uses in config/otel-collector.yaml. Only one can listen, so do not start ShadowClaw's collector alongside it.

The two products do not contend at that collector, because they arrive by different routes:

Product Transport Port
DefenseClaw's local-otlp destination gRPC, all three signals multiplexed 4317
ShadowClaw HTTP 4318

Both land in the same Loki, which is what makes the side-by-side row possible.

Leave metrics enabled here

Unlike the direct-to-Loki path, this collector has a real metrics pipeline into Prometheus remote write. --no-otlp-metrics would only throw away host telemetry.

up is not read-only — it wires config.yaml. That edit is made by DefenseClaw's own supported CLI, not by anything in ShadowClaw. up --no-config skips it, and down --disable-config reverses it.

What the dashboard gives you

ShadowClaw — Shadow AI Detections, in five rows.

Row Contents
Top Findings, criticals, distinct providers reached, processes implicated, peak risk score, and whether the sensor is currently reporting.
How risk escalated Risk score over time per provider — the view that shows a detection climbing as corroborating signals accumulate.
Detection detail Severity breakdown over time, and the findings themselves. Expand any line for process identity, endpoint, signals, and attribution confidence.
Side by side with DefenseClaw AI Discovery Two read-only panels for correlation: their discovery-scan lifecycle on the left, their security findings on the right.
Host plane Kill chains, credential reads, identities created, persistence, exfil surfaces, tactics over time by chain stage, agents ranked by tactic count, and the per-tactic activity timeline.
Kinetic Trust Protocol The four Risk Factors, each qualified by how much of them could be seen.
Deduplication What DefenseClaw already accounts for, what it has never seen, and whether it could be asked at all.

The provider panels read a different event

A finding names one provider; a host reaches several.

A finding carries a single headline provider, chosen by ranking its endpoints — unsanctioned egress first, then a named provider, then confidence, then hostname alphabetically. That is the right field for "what do I look at first" and the wrong one for "which providers did this host reach".

Grouping by (provider) over shadowclaw.finding.recorded counts only the winner. And because the choice is a sort rather than a race, the loser is not merely under-reported — it is invisible permanently.

Observed on a live agent swarm

A single Python process reaching Anthropic and Fireworks headlined Anthropic 584 times out of 584 and Fireworks never, purely because api.anthropic.com sorts before api.fireworks.ai. The same collapse was hiding github_copilot behind a co-occurring provider.

The failure mode is the dangerous kind: not an error, not an empty panel, just a smaller number that looks plausible.

So the sensor emits shadowclaw.provider.reached — one record per distinct provider a finding reached — and the three provider panels read that instead. One record per provider, not per endpoint, so a provider behind four rotating CDN addresses still counts once.

severity and risk_score are carried from the finding so the dashboard's severity filter selects the same population on these panels as on the findings panels. is_headline marks the one that would have been the finding's provider — filtering to is_headline="false" shows exactly what the old panels were dropping.

Unattributed egress keeps reading shadowclaw.finding.recorded with provider = "", because a finding with no attributed provider emits no provider record and would otherwise disappear from the chart.

The host-plane row

That row reads shadowclaw.agent.activity, a different event shape from the findings above it, so every panel filters on event_name explicitly rather than inheriting the stream.

An empty host-plane row means the host plane is off, not that the host is clean

It renders empty unless the sensor runs with enable_esf under root. shadowclaw.esf.running in the metrics store is the authoritative answer to which.

Correlating with DefenseClaw's own events

DefenseClaw 0.8.10 emits no ai_component.* event under any name. Its log vocabulary is ai.discovery.completed, finding.observed, scan.completed, hook_decision, tool.invocation.completed, guardrail.evaluation, correlation.relationship.changed, subsystem.lifecycle, tool_start, and tool_end.

A panel filtering on component names renders empty against a stream that is arriving perfectly well — which is what the row did until the filter was corrected to:

event_name=~"ai[.]discovery[.].*|finding[.]observed"

That finding was version-specific and has since been overtaken

Current DefenseClaw documentation describes "canonical v8 ai_component.* logs" carrying bounded model provenance for local_model signals, so a newer gateway may emit the names the original panel was looking for. The corrected filter is still the right one to ship — it matches what 0.8.10 sends and does not break on a version that sends more — but on a v8 gateway, add ai_component.* to the alternation rather than assuming the panel is broken.

Those records are multi-kilobyte nested JSON, so each panel also applies a line_format. Without it a logs panel is a wall of text and the row is no more readable full than it was empty.

Two panels, because the AIBOM volume buries everything else

ai.discovery.* and finding.observed are different record shapes, and rendering both from one query meant a line_format with a conditional in it — and, worse, one population burying the other.

aibom-claw.* rule ids are the reason. They are INFO-level AIBOM inventory enumerations — Tools (0) against ~/.cursor/mcp.json — emitted once per connector config per category, so on a host with several agents installed they dominate a shared list by volume while carrying no security conclusion.

If you only ever read the findings panel for real conclusions:

| body_defenseclaw_finding_rule_id !~ "aibom-claw.*"

Left out of the shipped query by default, because an AIBOM row is still evidence about what a connector declares, and this row exists to be read as a comparison rather than pre-filtered on our opinion of their scanner.

What each tool is for

DefenseClaw's AI Discovery documentation closes a section with a sentence worth borrowing:

Discovery tells you what's there. The Registry tells you what's allowed.

ShadowClaw adds the third clause: and what actually ran.

A complement rather than a comparison — but worth stating plainly, because the surfaces do overlap in places and anyone running both should know where each is authoritative.

Discovery is scoped to presence, deliberately

The clearest statement of the boundary is DefenseClaw's own. Its AI Discovery page carries a What DefenseClaw did — and did not do panel, and the did-not column includes:

Prove every detected component is active

with the summary immediately below:

Discovery reports evidence and confidence; it does not prove that every detected component is active or safe.

Read that as engineering discipline, not as a gap

An inventory that quietly implied liveness would be the more dangerous product. Discovery answers "what AI is installed or running on this machine?" across sixteen signal categories, with separate identity and presence confidence, per-detector evidence trails, model provenance down to the country of the root weights, and a compliance-shaped AIBOM. ShadowClaw does none of that and has no ambition to.

What it leaves open is the next question. An inventory establishes that a component exists; it does not establish that this process, at this moment, sent data to that provider. Discovery establishes presence and identity; ShadowClaw establishes behaviour.

Three places the boundary is load-bearing

Each is DefenseClaw's documented design, not an observed shortcoming.

Process discovery matches basenames, not arguments. From their privacy and trust model:

Process discovery matches executable basenames, not argument text. On Darwin, where the kernel-backed short process name truncates long aliases, DefenseClaw reads the full ps command-line field only long enough to recover the executable basename, then discards the executable path and arguments.

That is a privacy boundary, and a defensible one — argv is where secrets leak. It also means an agent framework inside a generic interpreter is invisible to it: a python3 importing langchain or crewai, or an MCP server started as npx mcp-server-github, presents a basename that says nothing. ShadowClaw's tactics.AGENT_CMDLINE_PATTERNS exists for that shape, and it redacts argv on the way out rather than declining to look.

provider_domain is host-wide DNS resolution. Their category table describes it as "DNS resolutions to known provider endpoints" — which answers "was this provider reached from this host". ShadowClaw attributes egress per pid, joining lsof sockets to sniffed DNS answers with graded confidence, so it can answer "which process reached it, and how sure are we of the name". Different owners in an incident.

There is no kill chain, because that is not what an inventory is. Nothing in Discovery sequences credential access → identity creation → privilege escalation → persistence → exfiltration against agent lineage. That is ShadowClaw's Plane C, and it is the part that turns four unremarkable events into one incident.

Where the two really do overlap

Worth naming plainly, because a document claiming no overlap would not be credible.

DefenseClaw's local_ai_endpoint detector probes vetted loopback runtimes — Ollama's /api/tags and /api/ps, LM Studio, LocalAI, vLLM, Lemonade — and --mode enhanced is explicitly aimed at shadow-model discovery. For the narrow question "is a model resident in Ollama right now", DefenseClaw answers directly, with model IDs and provenance ShadowClaw cannot produce.

The methods still differ. DefenseClaw asks the server; ShadowClaw watches the process. Asking is more precise when the server answers and blind when it does not — which their own runtime table concedes for llama.cpp: "No built-in model-metadata endpoint — Process and filesystem discovery only." A renamed inference binary with no metadata route is a sustained-compute signature and a loopback client, which is Plane A.

This overlap is why corroboration matches on local_model at all. Where both tools see the same subject, agreement is worth something.

The reading that needs both

Question Answered by
What AI is present? DefenseClaw Discovery / AIBOM
What AI is allowed? DefenseClaw Registry
What AI actually ran, and where did it send data? ShadowClaw

The two disagreeing is informative in both directions:

  • Present but never running — dormant attack surface. DefenseClaw sees it; ShadowClaw never will, because there is no behaviour to observe.
  • Running and egressing but not present — an agent that arrived by a path the inventory does not cover. ShadowClaw sees it, and scores it: uninventoried_local_model, +20.

Neither tool produces that pair alone, which is the argument for running both.

Corroboration: fewer findings, and louder ones

Everything above is one-way — ShadowClaw writes into the shared Loki and only renders DefenseClaw's stream beside its own. Corroboration is the one place the integration runs the other way.

sanctioned_endpoints already lets an operator declare that AI through a named gateway is approved, dropping the finding to a weight of 15: labelled, still ledgered, below the reporting floor on its own. DefenseClaw scans this host about once a minute, so where its inventory and ShadowClaw's runtime observation describe the same subject, "this AI is known" can be evidenced instead. That is a third state between sanctioned and shadow: inventoried.

It reads one 66 KB JSON file

~/.defenseclaw/ai_discovery_state.json, rewritten in place after each scan. A stat and a JSON parse per poll — no SQLite connection, no WAL, no lock, nothing that can contend with the writer.

Not their findings table, because there isn't one to match

audit.db is 1.6 GB, and on the development host its findings and network_egress_events tables were empty; scan_findings held only INFO-level aibom-claw inventory notes about config files. The two products do not emit the same kind of object — ShadowClaw scores runtime correlations, DefenseClaw inventories installed components. The overlap is at the level of the subject, not the finding.

Under sudo, ~ is /var/root

Which is not where DefenseClaw's state lives. Resolving the tilde would report the inventory as absent on exactly the privileged runs that produce the most findings — and silently, because absent is a legitimate outcome. SUDO_USER is preferred for that reason.

Two categories, deliberately

DefenseClaw category Matched against
local_model Plane A local-inference signals
mcp_server agent_local_mcp_server

Everything else is refused. Vendor is dense — all 49 signals on the development host carried one — and too coarse: DefenseClaw having seen Anthropic somewhere is not evidence about which process just opened a socket to api.anthropic.com. Paths are unavailable rather than rejected, because privacy_mode: enhanced stores hmac-sha256: hashes with only the basename in cleartext. active_process looks ideal and is not — it is the only category carrying a real pid, and exactly 1 of 49 signals had one.

Both directions

Inventory says Effect Tag
Accounts for it Local-inference weights halved corroboration:inventoried
Fresh, accounts for none of it uninventoried_local_model +20 / uninventoried_mcp_server +25 corroboration:unaccounted
Absent, stale, unparseable nothing corroboration:unobserved

The middle row is the point. DefenseClaw's model_file detector walks the filesystem, so sustained inference on a host where that walk found no model artifact means the weights are not somewhere a scanner can see them.

Only the corroborated signal group is re-weighted. A finding that also reached a provider keeps its egress weight in full; a chain that also read a credential keeps that weight in full.

A halved score can fall through the reporting floor, and that is the mechanism

A bare local_model_runtime is 30, halves to 15, and stops being emitted — the same inventoried, not alerted outcome as sanctioned_endpoints. A runtime that is also visibly inferring is 65 and attenuates to 33, so it drops from high to medium instead of vanishing. The more independent evidence ShadowClaw has of its own, the less another product's inventory can quiet it.

Never a discount for a blind corroborator

Unobserved is never spent as evidence or as exoneration

An absent, stale or unparseable inventory changes nothing — findings score exactly as they do with the feature off. A detector whose numbers fall because another product crashed is reporting "quiet" for "blind", and DefenseClaw stopping is not evidence about this host. This is the same rule the Risk Factors obey.

The blindness is still recorded, as corroboration:unobserved with a corroboration_state of absent / stale / unreadable / off. Not scoring on it is not the same as not saying so.

Staleness comes from the file's mtime, not the updated_at inside it. Two clocks are involved and only one is ours; an 85-minute disagreement was observed on the development host, which would have marked a file written seconds ago as long stale.

The banner states the position either way:

  corroboration   : DefenseClaw inventory, fresh 27s ago: 12 local model(s), 4 MCP server(s)
  corroboration   : ABSENT -- scores unaffected (/Users/you/.defenseclaw/ai_discovery_state.json)

Expect the host-level MCP match

mcp_server_host is the common case, and not because the matching is weak. An MCP server process matches MCP_PROCESS_PATTERN, making it an agent in its own right, so a session launched as npx mcp-server-github is rooted at npx — a name DefenseClaw never declares. Claiming the owning agent would mean overriding an attribution the chain engine deliberately made. mcp_server_agent fires when the session genuinely is a named agent.

Configuration

{
  "enable_corroboration": true,
  "corroboration_state_path": "",
  "corroboration_max_age_seconds": 300.0
}

The dashboard's Deduplication row is added by scripts/add-corroboration-panels.py. Corroboration unavailable sits leftmost, for the same reason Inputs unobserved does in the KTP row: it decides whether the two beside it mean anything.

Identity on the wire

ShadowClaw stamps service.name=shadowclaw and service.namespace=shadowclaw on every export, and its log events use shadowclaw.* event names.

ShadowClaw could emit under DefenseClaw's service identity and metric names, which would make its findings appear inside DefenseClaw's native AI Discovery panels. But its detections would then be indistinguishable from DefenseClaw's own, and neither tool's telemetry would be reliable as evidence.

In a detector whose attribution is tamper-evident by design, that trade is not worth making. So the two data sets are kept separate on the wire and correlated only at the presentation layer, in the dashboard's side-by-side row.

Removal

python3 integrations/defenseclaw/install_dashboard.py --remove

Deletes the ShadowClaw dashboard, plus the ShadowClaw folder — and only if you have not filed anything else in it.

The whole-host uninstaller goes further, and removes the shadowclaw-loki destination from DefenseClaw's config through DefenseClaw's own CLI (defenseclaw setup observability remove) rather than by editing config.yaml. That file usually holds destinations that are not ours, and hand-editing it would mean guessing which. See Uninstall.

Next