Ar9av/slackledger

Turn Slack conversations into an audit ledger: decisions, commitments, approvals, with who/when/where and person disambiguation.

★ 0Forks 0PythonGitHub ↗Compare
audit-logbotclaudecompliancepythonslack

README

slackledger

Turn Slack conversations into an audit ledger.

slackledger is a self-hosted Slack bot that reads the channels it is invited to and records the things that matter for an audit: decisions, commitments, approvals, action items, handoffs and escalations, each tied to a real Slack user, a timestamp, a channel, and a permalink to the message it came from. Names mentioned in text ("ask sam", "Sam K said", @sam) are resolved to canonical Slack user IDs with a confidence score and a recorded resolution method, so the ledger answers who without guessing.

{"id": 41, "kind": "commitment", "summary": "sam.kim committed to ship the migration by Friday",
 "actor_id": "U01ABC", "channel": "C0DEF", "source_ts": ["1726790400.000100"],
 "permalinks": ["https://acme.slack.com/archives/C0DEF/p1726790400000100"],
 "occurred_at": "2026-09-20T00:00:00+00:00", "confidence": 0.9, "model": "claude-opus-5",
 "extracted_at": "2026-09-20T00:02:11+00:00",
 "subjects": [{"mention": "priya", "user_id": "U02XYZ", "confidence": 0.95, "method": "unique_name"}]}
  • Live via Slack Socket Mode (no public URL) and backfill of channel history.
  • SQLite by default, Postgres for real deployments. One env var switches.
  • LLM extraction with Claude, structured output, only records what is explicitly said and keeps the verbatim quote and source message(s).
  • Person disambiguation ladder: <@U…> mention → exact handle/name → unique first name among thread participants → channel members → workspace → LLM only when still ambiguous → else recorded as unresolved, never silently guessed.
  • Idempotent: re-running a backfill never duplicates events. Edits update, deletions are flagged (never purged) so the ledger stays a ledger.
  • Export as JSONL or CSV, filter by time, channel, kind, or person.

slackledger in 70 seconds

Quick start

pip install slackledger            # or: pip install "slackledger[postgres]"
slackledger init                   # pick a template, save tokens, verify Slack (about 2 minutes)
slackledger run                    # live, over Socket Mode
slackledger backfill "#channel" --days 7
slackledger export --format table  # or jsonl / csv

slackledger init asks four questions: what to record (a template), your Slack tokens, how to run the model (API key or your Claude Code login), and where to store the ledger. It writes .env (mode 600) and slackledger.toml, connects to Slack, and lists the channels the bot can see.

Templates

Pick the one that matches the job. Each is a starting taxonomy; edit slackledger.toml afterwards to rename kinds, add your own, or change sensitivity. The extraction prompt follows the file.

Template For Kinds Sensitivity
engineering Product and engineering teams: a decision and commitment log without ceremony decision, commitment, approval, action_item, handoff, escalation precise
compliance Regulated teams that need a complete, evidence-backed record the core six + attestation, exception, access_change thorough
management Managers tracking who promised what, by when commitment, action_item, decision, handoff, escalation, completion, slip balanced
incident On-call teams reconstructing an incident channel detection, hypothesis, mitigation, decision, escalation, handoff, resolution thorough

slackledger templates prints the same list. A kind the model invents that isn't in your file is recorded as other, never as a new kind.

Slack app

  1. Go to https://api.slack.com/apps → Create New App → From a manifest → paste manifest.yaml.
  2. Install to Workspace. Copy the Bot User OAuth Token (xoxb-…) to SLACK_BOT_TOKEN.
  3. Basic Information → App-Level Tokens → generate one with connections:write. Copy the xapp-… token to SLACK_APP_TOKEN.
  4. Invite the bot: /invite @slackledger in each channel you want audited.

Scopes requested and why are commented in manifest.yaml. Direct messages are not read.

Anthropic

Set ANTHROPIC_API_KEY (or log in with ant auth login). The default model is claude-opus-5; override with AUDIT_MODEL. Server-side refusal fallbacks are enabled.

No API key? If you have Claude Code installed and logged in, set AUDIT_LLM_BACKEND=claude-cli. Extraction then runs through claude -p --json-schema under your Claude Code login. It is slower and intended for trying things out, not for production volume.

Commands

Command What it does
slackledger init [--template X] [--yes] Guided setup: template, tokens, model backend, storage.
slackledger templates List the built-in use-case templates.
slackledger run Start the Socket Mode bot. Runs migrations first.
slackledger backfill "#channel" --days 30 Read history + threads, extract, store. Safe to re-run.
slackledger export [--since] [--until] [--channel] [--kind] [--user] [--format jsonl|csv|table] Print the ledger to stdout.
slackledger users sync Refresh the user directory.
slackledger db upgrade Apply schema migrations.

Configuration

Everything is an environment variable (a .env file is read too).

Variable Default Meaning
SLACK_BOT_TOKEN xoxb-…
SLACK_APP_TOKEN xapp-… (Socket Mode)
ANTHROPIC_API_KEY Anthropic key
AUDIT_DATABASE_URL sqlite:///./slackledger.db Any SQLAlchemy URL. Postgres: postgresql+psycopg://user:pw@host/db
AUDIT_MODEL claude-opus-5 Extraction / disambiguation model
AUDIT_LLM_BACKEND anthropic anthropic (SDK, needs API key) or claude-cli (claude -p, needs Claude Code login)
AUDIT_WINDOW_MSGS 10 Extract a thread after this many new messages…
AUDIT_WINDOW_SECS 120 …or after this many seconds of quiet
AUDIT_CHANNELS (all) JSON list of channel IDs to audit
AUDIT_LOG_JSON 0 Emit JSON log lines
AUDIT_CONFIG slackledger.toml Taxonomy, sensitivity, notes and channel list (written by init)

Docker

cp .env.example .env && $EDITOR .env
docker compose up -d          # bot + Postgres 16
docker compose exec bot slackledger export --format csv > ledger.csv

The image alone (SQLite in a volume): docker run --env-file .env -v ledger:/data ghcr.io/…/slackledger.

How it works

Slack events ─▶ bot.py ─▶ pipeline.ingest ─▶ messages table
                                 │  (debounce per thread: N msgs or T secs quiet)
                                 ▼
                        extract.py  (Claude, structured output)
                                 ▼
                        people.py   (resolve mentions → user IDs)
                                 ▼
                        repo.add_events (ON CONFLICT DO NOTHING on dedup_key)
  • domain.py – plain models shared by every layer.
  • db.py / migrations/ – SQLAlchemy schema, Alembic migrations, portable across SQLite and Postgres.
  • repo.py – the only code that talks to the database.
  • slack_gateway.py – the only code that talks to Slack's Web API.
  • extract.py – prompts, schemas, and the Anthropic client.
  • people.py – pure resolution ladder; unit-tested without any network.
  • pipeline.py – orchestration, used identically by the live bot and backfill.

Schema

users, messages (ground truth; edits update the row, deletes set deleted=true), reactions, audit_events (dedup_key is unique: channel + source messages + kind + normalised summary), event_subjects (one row per person referenced by an event, with method and confidence), cursors.

Event kinds

decision · commitment · approval · action_item · handoff · escalation · other

Resolution methods

method confidence meaning
mention 1.0 <@U…> in the text
exact 1.0 Handle, display name or real name matched exactly
unique_name 0.95 / 0.9 / 0.7 First name (or first + last initial) unique among thread participants / channel members / workspace
llm model's Several candidates; the model chose using the sentence and candidate profiles
ambiguous 0 Several candidates; the model declined
unresolved 0 No candidate at all

Privacy

  • Only channels the bot is explicitly invited to. No DMs, no group DMs.
  • Message text is stored (it is the evidence). Deleted messages are flagged, not removed; delete rows manually if a retention policy requires it.
  • users:read.email is optional; remove the scope from the manifest if you do not want emails stored.
  • Transcripts are sent to Anthropic for extraction. See their data policies for retention.

Development

uv venv && uv pip install -e ".[dev]"
ruff check src tests && mypy src && pytest
# also against Postgres:
docker run -d --name pg -e POSTGRES_PASSWORD=pw -p 55432:5432 postgres:16-alpine
POSTGRES_URL=postgresql+psycopg://postgres:pw@localhost:55432/postgres pytest

Schema changes: edit db.py, then slackledger db revision -m "describe change" and review the generated file in src/slackledger/migrations/versions/.

Roadmap (not built until someone needs it)

  • Read-only HTTP API / dashboard over the ledger
  • Natural-language queries ("what did Sam commit to this week?")
  • Mapping Slack users to external identities (HR, GitHub)

License

MIT

Issues