Skip to main content
reference·Doc 01 of 13

CLI Reference

Every verb, every flag, every exit code. Nine verbs (observe, consult, run, defend, clean, case, setup, trigger, flush) with synopsis, sub-commands, examples, tier classification, and cross-references. The first place to look when integrating bl into existing tooling.

CLI Reference

Every verb. Every flag. Every exit code. Authored against the v0.6.0 release.

bl is a single bash script (~10,700 lines) assembled from 26 numbered parts under src/bl.d/ via make bl. Nine top-level verbs dispatch from main() in 90-main.sh. Every verb except setup and --help/--version runs bl_preflight before the verb body.

This page is the canonical reference. For why the surface looks like this, see Architecture. For how to configure the runtime, see Configuration.

At a glance

VerbPurposeDefault tierSource part
bl observeRead-only evidence extraction (logs, fs, cron, proc, htaccess, firewall, sigs, substrate, bundle)read-only41-observe-collectors.sh, 42-observe-router.sh, 45-cpanel.sh
bl consultOpen / attach a case via the curatorread-only50-consult.sh
bl runExecute an agent-prescribed step (tier-gated)varies (per step)60-run.sh
bl defendApply an agent-authored defensive payload (modsec / firewall / sig)auto / suggested82-defend.sh
bl cleanDiff-confirmed remediation (htaccess / cron / proc / file)destructive83-clean.sh
bl caseInspect / log / close / reopen casesread-only / destructive (close)70-case.sh
bl setupProvision or sync the Anthropic workspacen/a (preflight bypass)84-setup.sh
bl triggerOpen a case from a hook-fired event (LMD)read-only29-trigger.sh
bl flushDrain queued outbox recordsread-only27-outbox.sh

Global conventions

  • Help bypass. bl -h | --help | help and bl <verb> --help both bypass preflight. bl setup --help works on an unseeded host.
  • Version. bl -v | --version prints bl 0.5.0.
  • JSONL on the wire. Every bl observe collector emits one record per line. See Evidence Format.
  • Tier authorship. The agent classifies every step in bl-case/<case>/pending/<id>.json. The Runner enforces the gate based on verb class plus tier — verb class always wins. clean.* is always destructive regardless of agent-asserted tier.
  • Schema validation. bl run validates the step JSON against schemas/step.json before executing.
  • BL_DISABLE_LLM=1 short-circuits every LLM call across the surface. The full BATS suite exports this.

Exit codes

CodeSymbolMeaning
0BL_EX_OKSuccess.
64BL_EX_USAGEBad arguments / unknown subcommand.
65BL_EX_PREFLIGHT_FAILMissing ANTHROPIC_API_KEY, curl, or jq; or /var/lib/bl/ unwritable.
66BL_EX_WORKSPACE_NOT_SEEDEDNo agent record in the workspace. Run bl setup --sync.
67BL_EX_SCHEMA_FAILA step or payload failed JSON-schema validation.
68BL_EX_TIER_GATE_DENIEDTier gate refused the action (unknown verb, destructive without --yes, etc.).
69BL_EX_UPSTREAM_ERROR5xx from Anthropic after retry exhaustion.
70BL_EX_RATE_LIMITED429 from Anthropic; outbox-queued for next preflight.
71BL_EX_CONFLICT409 from Anthropic on a CAS write; the agent-update path retries once before propagating.
72BL_EX_NOT_FOUNDTargeted resource (case, step, file) missing.

POSIX 1–63 reserved by convention; 73–79 reserved for future blacklight use; 80–127 unused; 128+ are signal-based exits inherited from bash.

Tier matrix

Every bl observe, bl consult, and bl case show/log/list is read-only by verb class. Every bl clean is destructive regardless of agent-asserted tier (verb class re-asserts). bl defend and bl run flow through the agent's action_tier field. See Action Tiers for the full matrix.

Verb / sub-verbVerb-class tierNotes
bl observe *read-onlyAuto-execute; never prompts.
bl consult *read-only
bl case show / list / logread-only
bl case closedestructiveAgent confirms open_questions = 0; brief written.
bl case reopensuggestedOperator confirmation.
bl run(delegated)Per-step tier from action_tier field; verb class re-asserts on clean.*.
bl defend firewall <ip>autoApply + 15-min veto window.
bl defend sigauto (after FP gate)Deterministic + Haiku 4.5 adjudication.
bl defend modsecsuggestedapachectl -t pre-flight; explicit --yes.
bl clean *destructiveDiff shown; backup written; --yes per-op.
bl setup *n/aPreflight bypass; idempotent.
bl trigger lmdread-onlyOpens / attaches a case; no mutation.
bl flush --outboxread-onlyDrain only.

bl observe

Read-only evidence extraction. Auto-runs (no confirm). Emits JSONL to stdout and appends structured output to the current case bundle.

Synopsis

bl observe <subcommand> [args] [flags]

Sub-commands

SubcommandPurpose
bl observe file <path>stat, magic, sha256, strings, file(1) on a target.
bl observe log apache --around <path> [--window <dur>] [--site <fqdn>]Time-window vhost log slice for the path. JSONL with apache.transfer records.
bl observe log modsec [--txn <id>] [--rule <id>] [--around <path>]ModSec audit A/B/F/H section walker.
bl observe log journal --since <time> [--grep <pattern>]journalctl extract.
bl observe cron --user <user> [--system]crontab -l piped through cat -v to reveal ANSI ESC[2J persistence.
bl observe proc --user <user> [--verify-argv]argv[0] vs /proc/<pid>/exe basename mismatch detection (gsocket-class).
bl observe htaccess <dir> [--recursive]Injected directive flagging.
bl observe fs --mtime-cluster <path> --window <N>s [--ext <list>]Cluster discovery within a time window.
bl observe fs --mtime-since <date> [--under <path>] [--ext <list>]Retrospective sweep.
bl observe firewall [--backend auto]APF / CSF / iptables / nftables enumeration.
`bl observe sigs [--scanner lmdclamav
bl observe substratecPanel / EasyApache / hosting-stack inventory (12 substrate.* records).
bl observe bundleAssemble all collected artifacts into an evidence bundle.

Common flags

FlagDefaultPurpose
--window <duration>variesTime window for log slicing (6h, 30m).
--around <path>Anchor a log slice to events touching this path.
--site <fqdn>(auto)Filter Apache by vhost.
--ext <list>Comma-list of extensions to filter (php,phtml,inc).
--scanner lmd|clamav|yara(all)Limit signature inventory to one engine.

Examples

# Time-window slice around a suspicious file
$ bl observe log apache --around /var/www/html/pub/media/catalog/product/.cache/a.php --window 6h

# Retrospective fleet sweep against APSB25-94 disclosure date
$ bl observe fs --mtime-since 2026-03-20T00:00:00Z --under /var/www/html --ext php

# Bundle everything collected on this host into a tar+gzip evidence pack
$ bl observe bundle

Tier

Read-only. Auto-execute. No confirm prompt.

Exit codes

0, 64 (bad args), 65 (preflight), 66 (workspace unseeded).

See also


bl consult

Case lifecycle: open a new case or attach the operator shell to an existing one. Wakes the curator session.

Synopsis

bl consult --new --trigger <path-or-event> [--notes "..."] [--dedup]
bl consult --attach <case-id>
bl consult --sweep-mode [--cve <id>]

Modes

ModePurpose
--newAllocate CASE-YYYY-NNNN (flock-serialized counter), materialize templates into bl-case, create a session via POST /v1/sessions with workspace + per-case Files attached, POST a wake event. Returns the case-id on stdout.
--attach <id>Format-guard CASE-YYYY-NNNN, probe hypothesis.md, set case.current to the matched case.
--sweep-modeRead-only inventory of closed cases; optional CVE filter against each case's hypothesis.

Flags

FlagDefaultPurpose
--trigger <path-or-event>Required for --new. Path to the artifact that prompted the case, or an event blob.
--notes "..."Free-text notes appended to the trigger record.
--dedupoffIf the trigger fingerprint matches an open case within lmd_trigger_dedup_window_hours, attach instead of opening a new one.
--cve <id>Sweep-mode only. Filter to cases referencing the named CVE in their hypothesis.

Examples

# Open a fresh case from a polyshell artifact
$ bl consult --new --trigger /var/www/html/pub/media/catalog/product/.cache/a.php
CASE-2026-0017

# Attach the shell to an existing case (sets case.current)
$ bl consult --attach CASE-2026-0017

# Retrospective sweep across closed cases for a specific CVE
$ bl consult --sweep-mode --cve APSB25-94

Tier

Read-only. No host mutation. Wakes the curator session.

Exit codes

0, 64, 65, 66, 69 (upstream), 70 (rate limit), 71 (case allocation collision after 50 retries), 72 (--attach target missing).

See also


bl run

Execute an agent-prescribed step under tier-gated safety.

Synopsis

bl run <step-id> [--yes] [--dry-run] [--unsafe] [--explain]
bl run --list
bl run --batch [--max <N>] [--yes]

Modes

ModePurpose
<step-id>Pull <step-id> from bl-case/<case>/pending/, validate against schemas/step.json, prompt per tier policy, execute, write the result, append to ledger + memstore.
--listSingle-fetch listing of all pending steps for the current case with their action tier and a one-line synopsis.
--batch [--max <N>]Execute up to <N> pending steps in order (default 16). Read-only and auto auto-run; suggested honours --yes; destructive still gates per-step.

Flags

FlagDefaultPurpose
--yesoffSkip the operator prompt at suggested tier. Destructive requires --unsafe --yes.
--unsafeoffRequired for unknown-tier (verb not in dispatch table) or destructive steps.
--dry-runoffShow the plan and the diff; take no action; write nothing.
--explainoffDump the step's reasoning field from the pending JSON before prompting.
--max <N>16Batch upper bound.

Examples

# Show the queue
$ bl run --list
s-0001  observe.modsec       read-only      confirm rule coverage before payload
s-0043  defend.modsec        suggested      polyshell double-ext staging
s-0044  clean.cron --user x  destructive    strip ANSI-obscured cron entry

# Auto-execute a read-only step
$ bl run s-0001

# Explicit-yes on a suggested ModSec rule
$ bl run s-0043 --yes

# Drain the queue (auto + read-only auto-run; suggested honours --yes)
$ bl run --batch --max 8 --yes

Tier

Delegated. The step's action_tier field controls the gate. The Runner re-asserts on verb class — clean.* is always destructive, unknown verbs always deny.

Exit codes

0, 64, 65, 66, 67 (schema fail), 68 (tier gate denied / unknown verb), 69, 70, 72 (step-id not in pending).

See also


bl defend

Apply an agent-authored defensive payload. Three families: ModSec rules, firewall blocks, scanner signatures.

Synopsis

bl defend modsec <rule-file-or-id> [--remove] [--rollback <event-id>]
bl defend firewall <ip> [--backend auto] [--case <id>] [--reason <str>] [--retire <duration>]
bl defend sig <sig-file> [--scanner lmd|clamav|yara|all]

Sub-commands

SubcommandPurpose
defend modsecapachectl -t pre-flight in the curator sandbox; on pass, symlink-swap into mods-enabled/ + apachectl graceful; on fail, rollback to previous symlink target. --remove reverses; --rollback <event-id> restores from ledger.
defend firewallAuto-detect APF / CSF / iptables / nftables; CDN safe-list pre-check (internal allowlist + WHOIS/ASN cache); apply; ledger entry with retire-hint.
defend sigFP-baseline gate (run sig against $BL_DEFEND_FP_CORPUS) → Haiku 4.5 adjudication of borderline hits → on 0 FP, append to scanner sig file and reload. Auto-tier iff FP gate passes.

Flags

FlagDefaultPurpose
--removeoffReverse the rule (modsec) or revoke the block (firewall).
--rollback <event-id>Restore from a prior ledger event.
--backend <name>autoForce apf, csf, iptables, or nftables.
--case <id>(current)Tie the action to a specific case.
--reason <str>Free-text reason recorded in the ledger entry.
--retire <duration>30dWhen the firewall block becomes eligible for the retire-sweep.
--scanner <name>allLimit defend sig to one engine.

Examples

# Apply a curator-authored ModSec rule (passes apachectl -t in sandbox first)
$ bl defend modsec /tmp/bl-CASE-2026-0017-941999.conf --yes

# Block an attacker IP with a 30d retire hint
$ bl defend firewall 203.0.113.42 --reason "polyshell campaign C2" --retire 30d

# Promote a curator-authored YARA sig (deterministic FP gate + Haiku adjudication)
$ bl defend sig /tmp/bl-polyshell-double-ext.yar --scanner yara

Tier

  • autodefend firewall <ip> (new block; CDN safe-listed); defend sig after FP gate passes.
  • suggesteddefend modsec (new rule).
  • destructivedefend modsec --remove.

Exit codes

0, 64, 65, 66, 67, 68, 69, 70, 72.

See also


bl clean

Diff-confirmed remediation. Backup before apply. Quarantine, never unlink. --undo round-trips.

Synopsis

bl clean htaccess <dir> [--patch <file>] [--dry-run]
bl clean cron --user <user> [--patch <file>] [--dry-run]
bl clean proc <pid> [--capture] [--dry-run]
bl clean file <path> [--reason <str>] [--dry-run]
bl clean --undo <backup-id>
bl clean --unquarantine <entry-id>

Sub-commands

SubcommandPurpose
clean htaccess <dir>Remove an injected directive block (e.g., <FilesMatch "\.php$"> routing). Diff shown; backup written; explicit --yes.
clean cron --user <user>Strip ANSI-obscured persistence from a user crontab. cat -v reveal happens before the diff.
clean proc <pid>/proc/<pid>/{cmdline,environ,exe,cwd,maps,status} + lsof snapshot; then SIGTERMSIGKILL. Default is capture-on.
clean file <path>Move (never unlink) the file to $BL_VAR_DIR/quarantine/<case>/<sha256>-<basename>. Manifest records original metadata.
--undo <backup-id>Restore from $BL_VAR_DIR/backups/<id>.
--unquarantine <entry-id>Restore a quarantined file to its original path.

Flags

FlagDefaultPurpose
--patch <file>Apply a specific patch file rather than letting the curator compute the diff in-session.
--captureonclean proc only. --capture=off skips the /proc snapshot.
--reason <str>Recorded in the manifest entry for clean file.
--dry-runoffShow the diff and the backup path, write nothing.

Diff prompt

For file-edit subcommands the prompt offers [y/N/diff-full/explain/abort]:

  • y — apply.
  • N — cancel; mark step operator-rejected (the curator sees this).
  • diff-full — show the whole before/after.
  • explain — dump the curator's reasoning field.
  • abort — cancel and emit a session warning until the case closes.

Examples

# Strip an injected FilesMatch block; diff shown, backup written
$ bl clean htaccess /home/sitefoo/public_html --yes

# Remove the entire cron entry an attacker hid via ANSI escape
$ bl clean cron --user sitefoo --yes

# Capture /proc state then SIGTERM → SIGKILL the process
$ bl clean proc 31415

# Quarantine a polyshell sample (file moves to /var/lib/bl/quarantine/<case>/)
$ bl clean file /var/www/html/.cache/a.php --reason "polyshell APSB25-94"

# Roll back a clean operation
$ bl clean --undo 2026-04-26T04-27-15Z.htaccess

Tier

Always destructive by verb class. Agent-asserted tier is ignored. Backup written before every apply; quarantine-not-delete enforced for clean file.

Exit codes

0, 64, 65, 66, 67, 68, 72.

See also


bl case

Inspect / log / close / reopen. Cases are allocated by bl consult --new — there is no bl case open.

Synopsis

bl case list [--open|--closed|--all]
bl case show [<case-id>]
bl case log [<case-id>] [--audit]
bl case close [<case-id>] [--force]
bl case reopen <case-id> --reason <str>

Sub-commands

SubcommandPurpose
listWorkspace case roster on this host.
showSix-section summary: hypothesis, evidence, pending, applied, defenses, ledger tail.
logFull chronological ledger. --audit appends per-kind summary + decoded fence-wrapped wake entries from outbox.
closeAgent validates open_questions = 0, renders the brief (Markdown canonical, HTML/PDF best-effort via curator sandbox). T+30d retire-sweep scheduled for applied firewall blocks.
reopenRe-attach a closed case to its session. Requires --reason.

Flags

FlagDefaultPurpose
--open / --closed / --all--allFilter for bl case list.
--auditoffbl case log only. Per-kind summary + outbox fence decode.
--forceoffbl case close only. Override the open-questions gate (operator-only escape).
--reason <str>Required for bl case reopen.

Examples

# List open cases on this host
$ bl case list --open

# Six-section summary of the current case
$ bl case show

# Audit-format ledger for a regulator
$ bl case log CASE-2026-0017 --audit

# Close after the curator confirms open_questions = 0
$ bl case close

# Reopen with a documented reason
$ bl case reopen CASE-2026-0017 --reason "T+5d sweep showed re-pivot"

Tier

list / show / log are read-only. close is destructive (writes the brief, schedules retires). reopen is suggested.

Exit codes

0, 64, 65, 66, 68, 69, 70, 72.

See also


bl setup

Workspace bootstrap. Idempotent. Preflight is bypassed (setup carries its own local preflight).

Synopsis

bl setup --sync [--dry-run]
bl setup --reset [--force]
bl setup --gc
bl setup --eval [--promote]
bl setup --check
bl setup --install-hook lmd
bl setup --import-from-lmd

Sub-commands

SubcommandPurpose
--syncProvision (or update) agent + Skills + Files. SHA-256 delta-check; only changed content is pushed.
--resetArchive the agent (POST /v1/agents/<id>/archive), delete Skills + workspace Files, clear identity from state.json. Preserves case_* keys so audit trail survives.
--gcWalk state.files_pending_deletion[]; delete each file_id only when no live session in state.session_ids references it.
--evalLive skill-routing eval (BL_EVAL_LIVE=1). --promote bumps the agent version on pass.
--checkPrint state.json snapshot + per-resource health. Hits no write endpoints.
--install-hook lmdDrop bl-lmd-hook into /etc/blacklight/hooks/ and edit conf.maldet's post_scan_hook=.
--import-from-lmdRead conf.maldet, write notify creds to /etc/blacklight/notify.d/* (mode 0600, metacharacter-rejected).

Flags

FlagDefaultPurpose
--dry-runoffPreview the calls --sync would make; hits no write endpoints.
--forceoffRequired for destructive surfaces (--reset).
--promoteoffBump the agent CAS version on a passing eval.

Live-API contract (M15)

  • Agent update: POST /v1/agents/<id> with {version: <current>, ...}. PATCH returns 405. On 409 the client refetches and retries once.
  • Agent retire: POST /v1/agents/<id>/archive. DELETE returns 405. Empty {} body. --reset aborts if archive fails — never wipe state.json while a live agent exists.
  • Sessions.create body: {agent: <id>, environment_id: <id>, resources: [...]}. The field is agent, not agent_id.
  • Environments body: {name, config:{type:"cloud", networking:{type:"unrestricted"}}} only. No packages, setup_script, or image field.

Examples

# First-run on a fresh workspace (or no-op on a seeded one)
$ export ANTHROPIC_API_KEY="sk-ant-..."
$ bl setup --sync

# Idempotent re-sync after editing skills
$ bl setup --sync

# Wipe everything except case audit trail
$ bl setup --reset --force

# Wire LMD's post_scan_hook to bl trigger
$ bl setup --install-hook lmd

Tier

Preflight bypass. --sync and --gc are non-destructive at workspace scope; --reset requires --force.

Exit codes

0, 64, 65, 66 is impossible (this is the verb that seeds it), 67, 69, 70, 71 (CAS conflict, retried internally).

See also


bl trigger

Hook-fired case open from existing trigger surfaces. Today: LMD post_scan_hook.

Synopsis

bl trigger lmd --scanid <id> [--session-file <path>] [--source-conf <path>] [--unattended]

Sub-commands

SubcommandPurpose
lmd --scanid <id>Parse LMD per-session TSV → JSONL; compute cluster fingerprint sha256(scanid || sigs || paths)[:16]; bl_consult_new --dedup with lmd_trigger_dedup_window_hours from blacklight.conf (default 24h). Degraded path on empty/unreadable TSV opens a stub case anyway.

Flags

FlagDefaultPurpose
--scanid <id>Required. The maldet scan id.
--session-file <path>(auto)Override for the LMD session TSV path.
--source-conf <path>(auto)Override for the maldet conf used to resolve paths.
--unattended(auto)Force unattended mode regardless of TTY detection.

Examples

# Manual replay of an LMD hook-fire (LMD itself calls this from post_scan_hook)
$ bl trigger lmd --scanid 250426-141520.31415

# Force unattended (queue tier-gated steps for operator instead of prompting)
$ bl trigger lmd --scanid 250426-141520.31415 --unattended

Tier

Read-only at the trigger layer. The case it opens may emit destructive steps; the gate catches those at bl run time, not here.

Exit codes

0, 64, 65, 66, 69, 70.

See also


bl flush

Drain queued outbox records (wake / signal_upload / action_mirror). Idempotent. Safe from cron.

Synopsis

bl flush --outbox

Flags

FlagDefaultPurpose
--outboxDrain $BL_VAR_DIR/outbox/.

Drain policy

Preflight runs an age-gated drain automatically when the oldest queued record is ≥ BL_OUTBOX_AGE_WARN_SECS (default 3600). bl flush --outbox forces a drain regardless of age — useful from cron, useful when chasing a queued record.

Watermarks: warn at 500 records, hard-stop at 1000 (returns BL_EX_RATE_LIMITED=70). The cycle-break invariant in 27-outbox.sh keeps the queue's own backpressure-reject record from re-entering the ledger via the outbox path.

Examples

# Drain manually
$ bl flush --outbox

# From cron, every 15 minutes
*/15 * * * * /usr/local/bin/bl flush --outbox

Tier

Read-only at the verb level. The outbox replay re-issues network calls that may individually mutate workspace state (those are tier-checked at write time).

Exit codes

0, 64, 65, 69, 70.

See also


Bash SDK

bl is built as a bash SDK that the CLI dispatcher consumes. Sourcing the assembled binary (source bl || true) exposes 137 bl_* functions across 14 families. Stable surfaces: bl_api_*, bl_files_*, bl_skills_*, bl_ledger_*, bl_outbox_*, bl_fence_*, bl_messages_call, bl_notify. Internal: everything else; subject to rename at any milestone.

Operators consuming the SDK should pin to a specific tagged version of bl. Full surface enumeration in Architecture · Bash SDK.


See also

  • Configuration — env vars, state.json, /var/lib/bl/, /etc/blacklight/blacklight.conf.
  • Architecture — three-layer contract, runtime flow, SDK surface.
  • Setup Flow — endpoint-by-endpoint contract for bl setup --sync.
  • Action Tiers — gate matrix for bl run / bl defend / bl clean.
  • Security Model — what the gate catches.