diff --git a/Cargo.lock b/Cargo.lock
index ffaf13f..5b9a869 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2172,6 +2172,7 @@ dependencies = [
  "predicates",
  "qrcode",
  "rand",
+ "regex",
  "relicario-core",
  "reqwest",
  "rpassword",
diff --git a/crates/relicario-cli/Cargo.toml b/crates/relicario-cli/Cargo.toml
index db05181..928004c 100644
--- a/crates/relicario-cli/Cargo.toml
+++ b/crates/relicario-cli/Cargo.toml
@@ -31,10 +31,11 @@ rqrr = "0.7"
 reqwest = { version = "0.12", features = ["blocking", "json"] }
 qrcode = { version = "0.14", features = ["svg"] }
 ssh-key = { version = "0.6", features = ["ed25519", "std"] }
+regex = "1"
+tempfile = "3"
 
 [dev-dependencies]
 assert_cmd = "2"
 predicates = "3"
-tempfile = "3"
 serde_json = "1"
 ed25519-dalek = "2"
diff --git a/crates/relicario-cli/src/commands/org.rs b/crates/relicario-cli/src/commands/org.rs
index b0f1bf8..799b14b 100644
--- a/crates/relicario-cli/src/commands/org.rs
+++ b/crates/relicario-cli/src/commands/org.rs
@@ -329,6 +329,285 @@ fn resolve_member_id(members: &OrgMembers, prefix: &str) -> Result<MemberId> {
     }
 }
 
+// ═══════════ Status / Audit (B8) ═══════════
+
+/// `org status`: print the org's members + collections with no decryption. Reads
+/// the three plaintext metadata files (org.json, members.json, collections.json)
+/// directly — the manifest stays encrypted and is never touched.
+pub fn run_org_status(dir: &Path) -> Result<()> {
+    let root = crate::org_session::org_dir(Some(dir))?;
+
+    let meta: relicario_core::OrgMeta = {
+        let s = fs::read_to_string(root.join("org.json")).context("read org.json")?;
+        serde_json::from_str(&s).context("parse org.json")?
+    };
+    let members: OrgMembers = {
+        let s = fs::read_to_string(root.join("members.json")).context("read members.json")?;
+        serde_json::from_str(&s).context("parse members.json")?
+    };
+    let collections: OrgCollections = {
+        let s = fs::read_to_string(root.join("collections.json"))
+            .context("read collections.json")?;
+        serde_json::from_str(&s).context("parse collections.json")?
+    };
+
+    println!("Org: {} ({})", meta.display_name, meta.org_id.as_str());
+    println!();
+    println!("Members ({}):", members.members.len());
+    for m in &members.members {
+        let colls = if m.collections.is_empty() {
+            "(no collections)".to_string()
+        } else {
+            m.collections.join(", ")
+        };
+        println!(
+            "  {:?}  {}  {}  [{}]",
+            m.role,
+            m.member_id.as_str(),
+            m.display_name,
+            colls
+        );
+    }
+    println!();
+    println!("Collections ({}):", collections.collections.len());
+    for c in &collections.collections {
+        println!("  {}  —  {}", c.slug, c.display_name);
+    }
+    Ok(())
+}
+
+/// One audited org-vault commit, attributed to a VERIFIED git signer.
+#[derive(Debug, serde::Serialize)]
+pub struct AuditEvent {
+    pub commit: String,
+    pub timestamp: String,
+    /// Actor as resolved from the VERIFIED signing key (authoritative).
+    pub actor_name: Option<String>,
+    pub actor_id: Option<String>,
+    /// Actor id as CLAIMED by the commit trailer (advisory; for tamper-checking).
+    pub trailer_actor_id: Option<String>,
+    pub action: Option<String>,
+    pub collection: Option<String>,
+    pub item_id: Option<String>,
+    pub device_id: Option<String>,
+    /// True when the trailer's claimed actor disagrees with the verified signer,
+    /// or when no current member matches the signing key.
+    pub tampered: bool,
+}
+
+/// Parse a commit's `Relicario-*` trailer block into an `AuditEvent`. The actor
+/// id captured here is the trailer's CLAIM (`trailer_actor_id`) — the
+/// authoritative `actor_id` is resolved later from the verified signature.
+fn parse_trailer_block(commit: &str, timestamp: &str, trailers: &str) -> AuditEvent {
+    let mut ev = AuditEvent {
+        commit: commit.to_string(),
+        timestamp: timestamp.to_string(),
+        actor_name: None,
+        actor_id: None,
+        trailer_actor_id: None,
+        action: None,
+        collection: None,
+        item_id: None,
+        device_id: None,
+        tampered: false,
+    };
+    for line in trailers.lines() {
+        let line = line.trim();
+        if let Some(rest) = line.strip_prefix("Relicario-Actor:") {
+            // Contract format: "<name> <member_id>" (member_id is the last token).
+            let rest = rest.trim();
+            if let Some((_name, id)) = rest.rsplit_once(' ') {
+                ev.trailer_actor_id = Some(id.trim().to_string());
+            } else if !rest.is_empty() {
+                ev.trailer_actor_id = Some(rest.to_string());
+            }
+        } else if let Some(v) = line.strip_prefix("Relicario-Action:") {
+            ev.action = Some(v.trim().to_string());
+        } else if let Some(v) = line.strip_prefix("Relicario-Collection:") {
+            ev.collection = Some(v.trim().to_string());
+        } else if let Some(v) = line.strip_prefix("Relicario-Item:") {
+            ev.item_id = Some(v.trim().to_string());
+        } else if let Some(v) = line.strip_prefix("Relicario-Device:") {
+            ev.device_id = Some(v.trim().to_string());
+        }
+    }
+    ev
+}
+
+/// Resolve a commit's SSH signature fingerprint to a current member, mirroring
+/// the pre-receive hook: build an allowed_signers from members.json, inject it
+/// via GIT_CONFIG_*, run `git verify-commit --raw`, parse the SHA256: key from
+/// stderr. Returns None if the commit is unsigned or the signer is not a member.
+fn resolve_signer<'m>(
+    root: &Path,
+    commit: &str,
+    members: &'m relicario_core::OrgMembers,
+) -> Option<&'m relicario_core::OrgMember> {
+    use std::io::Write;
+    let mut tmp = tempfile::NamedTempFile::new().ok()?;
+    for m in &members.members {
+        let _ = writeln!(tmp, "relicario {}", m.ed25519_pubkey.trim());
+    }
+    let allowed_path = tmp.path();
+
+    let output = std::process::Command::new("git")
+        .current_dir(root)
+        .args(["verify-commit", "--raw", commit])
+        .env("GIT_CONFIG_COUNT", "1")
+        .env("GIT_CONFIG_KEY_0", "gpg.ssh.allowedSignersFile")
+        .env("GIT_CONFIG_VALUE_0", allowed_path.as_os_str())
+        .output()
+        .ok()?;
+    let stderr = String::from_utf8_lossy(&output.stderr);
+
+    let re = regex::Regex::new(r"key (SHA256:[A-Za-z0-9+/]+)").ok()?;
+    let fp = re.captures(&stderr)?.get(1)?.as_str().to_string();
+
+    members.members.iter().find(|m| {
+        relicario_core::fingerprint(&m.ed25519_pubkey).ok().as_deref() == Some(fp.as_str())
+    })
+}
+
+/// `org audit`: parse `git log`, resolve each commit's VERIFIED signer to a
+/// member and report THAT as the actor (trailers are advisory), flag
+/// trailer/signer mismatch as `TAMPERED`, and frame records with `%x1e`/`%x1f`
+/// (so multi-line trailer values cannot misalign records) using the committer
+/// date (`%cI`).
+pub fn run_org_audit(
+    dir: &Path,
+    since: Option<&str>,
+    member_filter: Option<&str>,
+    collection_filter: Option<&str>,
+    action_filter: Option<&str>,
+    format: &str,
+) -> Result<()> {
+    // Spec surface is `--format <table|json>` (default table). Accept only those.
+    let json = match format {
+        "json" => true,
+        "table" => false,
+        other => anyhow::bail!("unknown --format `{other}` — use table or json"),
+    };
+    let root = crate::org_session::org_dir(Some(dir))?;
+
+    // members.json — needed to resolve each commit's verified signer to a member.
+    let members: relicario_core::OrgMembers = {
+        let s = fs::read_to_string(root.join("members.json")).context("read members.json")?;
+        serde_json::from_str(&s).context("parse members.json")?
+    };
+
+    // git log framed with a record separator (%x1e, U+001E) PER COMMIT and a
+    // field separator (%x1f, U+001F) between fields, so multi-line trailer
+    // values cannot misalign record boundaries. Committer date (%cI), not
+    // author date: it is what revocation/audit is anchored to.
+    let fmt = "%x1e%H%x1f%cI%x1f%(trailers:only=true,unfold=true)";
+    let mut args: Vec<String> = vec!["log".into(), format!("--format={fmt}")];
+    if let Some(s) = since {
+        args.push(format!("--since={s}"));
+    }
+
+    let output = std::process::Command::new("git")
+        .current_dir(&root)
+        .args(&args)
+        .output()
+        .context("git log")?;
+    let log = String::from_utf8_lossy(&output.stdout);
+
+    let events = parse_audit_log(&root, &log, &members, member_filter, collection_filter, action_filter);
+
+    if json {
+        println!("{}", serde_json::to_string_pretty(&events)?);
+    } else {
+        println!(
+            "{:<44} {:<26} {:<20} {:<18} {}",
+            "COMMIT", "TIMESTAMP", "ACTION", "ACTOR", "FLAG"
+        );
+        for ev in &events {
+            println!(
+                "{:<44} {:<26} {:<20} {:<18} {}",
+                ev.commit,
+                ev.timestamp,
+                ev.action.as_deref().unwrap_or("-"),
+                ev.actor_name.as_deref().unwrap_or("<unverified>"),
+                if ev.tampered { "TAMPERED" } else { "" },
+            );
+        }
+    }
+    Ok(())
+}
+
+/// Frame a raw `git log` body (records split on `%x1e`, fields on `%x1f`) into
+/// attributed `AuditEvent`s. Each commit's VERIFIED signer is resolved via
+/// `resolve_signer` and reported as the authoritative actor; trailer/signer
+/// disagreement (or no matching member) sets the `tampered` flag. Filters apply
+/// to the VERIFIED actor id, not the spoofable trailer. Split out from
+/// `run_org_audit` so it can be unit-tested over a real signed repo.
+fn parse_audit_log(
+    root: &Path,
+    log: &str,
+    members: &relicario_core::OrgMembers,
+    member_filter: Option<&str>,
+    collection_filter: Option<&str>,
+    action_filter: Option<&str>,
+) -> Vec<AuditEvent> {
+    let mut events: Vec<AuditEvent> = Vec::new();
+    for record in log.split('\u{1e}') {
+        let record = record.trim_start_matches('\n');
+        if record.trim().is_empty() {
+            continue;
+        }
+        let mut fields = record.splitn(3, '\u{1f}');
+        let commit = fields.next().unwrap_or("").trim();
+        let ts = fields.next().unwrap_or("").trim();
+        let trailers = fields.next().unwrap_or("");
+        if commit.is_empty() {
+            continue;
+        }
+
+        let mut ev = parse_trailer_block(commit, ts, trailers);
+        if ev.action.is_none() {
+            continue; // not an org commit
+        }
+
+        // Resolve the VERIFIED signer and attribute it as the authoritative actor.
+        match resolve_signer(root, commit, members) {
+            Some(m) => {
+                ev.actor_name = Some(m.display_name.clone());
+                ev.actor_id = Some(m.member_id.as_str().to_string());
+                // Tampered if the trailer claims a different actor than the signer.
+                if let Some(claimed) = ev.trailer_actor_id.as_deref() {
+                    if claimed != m.member_id.as_str() {
+                        ev.tampered = true;
+                    }
+                }
+            }
+            None => {
+                // No current member matched the signature -> cannot trust the
+                // trailer's claimed actor.
+                ev.tampered = true;
+            }
+        }
+
+        if let Some(mid) = member_filter {
+            // Filter on the VERIFIED actor id, not the spoofable trailer.
+            if ev.actor_id.as_deref() != Some(mid) {
+                continue;
+            }
+        }
+        if let Some(col) = collection_filter {
+            if ev.collection.as_deref() != Some(col) {
+                continue;
+            }
+        }
+        if let Some(act) = action_filter {
+            if ev.action.as_deref() != Some(act) {
+                continue;
+            }
+        }
+        events.push(ev);
+    }
+    events
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -385,4 +664,201 @@ mod tests {
         assert!(!members.find_by_id(&id).unwrap().collections.contains(&"prod".to_string()));
         assert!(members.find_by_id(&id).unwrap().collections.contains(&"dev".to_string()));
     }
+
+    // ───── Status / Audit (B8) ─────
+
+    #[test]
+    fn parse_trailers_extracts_relicario_fields() {
+        // Contract trailer shape: "Relicario-Actor: <name> <member_id>".
+        let raw = "Relicario-Actor: alice a1b2c3d4e5f6a1b2\nRelicario-Action: item-create\nRelicario-Collection: prod\n";
+        let event = parse_trailer_block("abc123", "2026-06-06T12:00:00+00:00", raw);
+        assert_eq!(event.action.as_deref(), Some("item-create"));
+        assert_eq!(event.collection.as_deref(), Some("prod"));
+        // The verified actor_id is resolved later from the signature, not the trailer;
+        // the trailer only populates trailer_actor_id here.
+        assert_eq!(event.trailer_actor_id.as_deref(), Some("a1b2c3d4e5f6a1b2"));
+        assert_eq!(event.actor_id, None);
+        assert!(!event.tampered);
+    }
+
+    #[test]
+    fn parse_trailers_captures_item_and_device() {
+        let raw = "Relicario-Actor: bob feedfacefeedface\nRelicario-Action: item-update\nRelicario-Item: 0123456789abcdef\nRelicario-Device: laptop\n";
+        let ev = parse_trailer_block("def456", "2026-06-06T13:00:00+00:00", raw);
+        assert_eq!(ev.action.as_deref(), Some("item-update"));
+        assert_eq!(ev.item_id.as_deref(), Some("0123456789abcdef"));
+        assert_eq!(ev.device_id.as_deref(), Some("laptop"));
+        assert_eq!(ev.trailer_actor_id.as_deref(), Some("feedfacefeedface"));
+    }
+
+    #[test]
+    fn parse_trailers_single_token_actor_falls_back_to_whole_value() {
+        // No space => the whole value is treated as the member id.
+        let raw = "Relicario-Actor: lonelytoken00000\nRelicario-Action: org-init\n";
+        let ev = parse_trailer_block("c0ffee", "2026-06-06T14:00:00+00:00", raw);
+        assert_eq!(ev.trailer_actor_id.as_deref(), Some("lonelytoken00000"));
+        assert_eq!(ev.action.as_deref(), Some("org-init"));
+    }
+
+    #[test]
+    fn parse_trailers_non_org_commit_has_no_action() {
+        // A commit with no Relicario-* trailers parses to an event with no action,
+        // which run_org_audit skips.
+        let ev = parse_trailer_block("beef", "2026-06-06T15:00:00+00:00", "");
+        assert!(ev.action.is_none());
+    }
+}
+
+#[cfg(test)]
+mod audit_log_tests {
+    //! Record-framing + filter tests for `parse_audit_log` against a synthetic
+    //! `git log` body (no real repo / signatures needed: members.json is empty so
+    //! `resolve_signer` always returns None and every org commit is flagged
+    //! TAMPERED — which is exactly the "signer is not a current member" path).
+    use super::*;
+    use relicario_core::OrgMembers;
+
+    /// Build one framed record: leading %x1e, then commit %x1f ts %x1f trailers.
+    fn record(commit: &str, ts: &str, trailers: &str) -> String {
+        format!("\u{1e}{commit}\u{1f}{ts}\u{1f}{trailers}")
+    }
+
+    #[test]
+    fn parse_audit_log_frames_records_and_flags_unverified() {
+        let members = OrgMembers::new(); // no members => no signer can resolve
+        let log = format!(
+            "{}{}",
+            record(
+                "1111111111111111111111111111111111111111",
+                "2026-06-06T12:00:00+00:00",
+                "Relicario-Actor: alice a1b2c3d4e5f6a1b2\nRelicario-Action: item-create\nRelicario-Collection: prod\n",
+            ),
+            record(
+                "2222222222222222222222222222222222222222",
+                "2026-06-06T13:00:00+00:00",
+                "Relicario-Actor: bob feedfacefeedface\nRelicario-Action: item-update\nRelicario-Collection: dev\n",
+            ),
+        );
+        // root path is unused once resolve_signer short-circuits on empty members,
+        // but verify-commit will run; point it at a tempdir to be safe.
+        let tmp = tempfile::tempdir().unwrap();
+        let events = parse_audit_log(tmp.path(), &log, &members, None, None, None);
+        assert_eq!(events.len(), 2);
+        // Leading %x1e produced an empty leading split element that was filtered.
+        assert_eq!(events[0].commit, "1111111111111111111111111111111111111111");
+        assert_eq!(events[0].action.as_deref(), Some("item-create"));
+        assert_eq!(events[0].collection.as_deref(), Some("prod"));
+        // No member matched the (absent) signature => TAMPERED, no verified actor.
+        assert!(events[0].tampered);
+        assert_eq!(events[0].actor_name, None);
+        assert_eq!(events[0].actor_id, None);
+        // Trailer claim is preserved for forensic comparison.
+        assert_eq!(events[0].trailer_actor_id.as_deref(), Some("a1b2c3d4e5f6a1b2"));
+    }
+
+    #[test]
+    fn parse_audit_log_skips_non_org_commits() {
+        let members = OrgMembers::new();
+        let log = format!(
+            "{}{}",
+            // A non-org commit: no Relicario-Action trailer.
+            record("3333", "2026-06-06T10:00:00+00:00", "Some-Other: trailer\n"),
+            record(
+                "4444",
+                "2026-06-06T11:00:00+00:00",
+                "Relicario-Action: org-init\nRelicario-Actor: alice a1b2c3d4e5f6a1b2\n",
+            ),
+        );
+        let tmp = tempfile::tempdir().unwrap();
+        let events = parse_audit_log(tmp.path(), &log, &members, None, None, None);
+        assert_eq!(events.len(), 1);
+        assert_eq!(events[0].commit, "4444");
+        assert_eq!(events[0].action.as_deref(), Some("org-init"));
+    }
+
+    #[test]
+    fn parse_audit_log_multiline_trailer_value_does_not_misalign() {
+        // A multi-line trailer value must not break record framing: only %x1e
+        // ends a record, not a newline inside the trailer block.
+        let members = OrgMembers::new();
+        let log = format!(
+            "{}{}",
+            record(
+                "5555",
+                "2026-06-06T09:00:00+00:00",
+                "Relicario-Action: item-create\nRelicario-Actor: carol cafecafecafecafe\nRelicario-Collection: prod\n",
+            ),
+            record(
+                "6666",
+                "2026-06-06T09:30:00+00:00",
+                "Relicario-Action: item-delete\nRelicario-Actor: dave deaddeaddeaddead\nRelicario-Collection: dev\n",
+            ),
+        );
+        let tmp = tempfile::tempdir().unwrap();
+        let events = parse_audit_log(tmp.path(), &log, &members, None, None, None);
+        assert_eq!(events.len(), 2);
+        assert_eq!(events[0].commit, "5555");
+        assert_eq!(events[1].commit, "6666");
+        assert_eq!(events[1].action.as_deref(), Some("item-delete"));
+    }
+
+    #[test]
+    fn parse_audit_log_collection_and_action_filters_apply() {
+        let members = OrgMembers::new();
+        let log = format!(
+            "{}{}{}",
+            record(
+                "7777",
+                "2026-06-06T08:00:00+00:00",
+                "Relicario-Action: item-create\nRelicario-Collection: prod\nRelicario-Actor: a aaaa000000000000\n",
+            ),
+            record(
+                "8888",
+                "2026-06-06T08:10:00+00:00",
+                "Relicario-Action: item-update\nRelicario-Collection: prod\nRelicario-Actor: a aaaa000000000000\n",
+            ),
+            record(
+                "9999",
+                "2026-06-06T08:20:00+00:00",
+                "Relicario-Action: item-create\nRelicario-Collection: dev\nRelicario-Actor: a aaaa000000000000\n",
+            ),
+        );
+        let tmp = tempfile::tempdir().unwrap();
+
+        // Collection filter: only prod commits survive.
+        let prod = parse_audit_log(tmp.path(), &log, &members, None, Some("prod"), None);
+        assert_eq!(prod.len(), 2);
+        assert!(prod.iter().all(|e| e.collection.as_deref() == Some("prod")));
+
+        // Action filter: only item-create commits survive.
+        let creates = parse_audit_log(tmp.path(), &log, &members, None, None, Some("item-create"));
+        assert_eq!(creates.len(), 2);
+        assert!(creates.iter().all(|e| e.action.as_deref() == Some("item-create")));
+
+        // Combined: item-create AND prod => just commit 7777.
+        let combined =
+            parse_audit_log(tmp.path(), &log, &members, None, Some("prod"), Some("item-create"));
+        assert_eq!(combined.len(), 1);
+        assert_eq!(combined[0].commit, "7777");
+    }
+
+    #[test]
+    fn parse_audit_log_member_filter_uses_verified_actor_not_trailer() {
+        // With no resolvable signer, actor_id is None, so a member filter naming
+        // the TRAILER's claimed id must NOT match — the filter is on the verified
+        // actor, which is the whole point of TAMPERED attribution.
+        let members = OrgMembers::new();
+        let log = record(
+            "aaaa",
+            "2026-06-06T07:00:00+00:00",
+            "Relicario-Action: item-create\nRelicario-Actor: mallory deadbeefdeadbeef\n",
+        );
+        let tmp = tempfile::tempdir().unwrap();
+        let filtered =
+            parse_audit_log(tmp.path(), &log, &members, Some("deadbeefdeadbeef"), None, None);
+        assert!(
+            filtered.is_empty(),
+            "member filter must match the verified actor id, never the spoofable trailer"
+        );
+    }
 }
