Skip to main content
Glama
README.md
# crewlog

**Give every teammate's coding agent shared company context — and one append-only
log of everything your crew (humans and agents) is doing.**

crewlog is an MCP server for teams running agentic coding tools (Claude Code,
OpenCode, Codex, or anything MCP-compatible). Each person's agent can see Linear
tasks, Slack discussion, and a shared **company activity log** that merges Slack
messages, Linear changes, call summaries ([Granola](https://granola.ai)
webhooks), and coding sessions into one timeline. A daily **coordinator** —
Claude on a cron — reads the log and writes a briefing that every agent
surfaces before planning the day. Ask your agent *"what should I do next?"* or
*"what did I miss?"* and it knows.

Modeled on Fleet AI's Code with Claude 2026 talk
[*"Claude & the self-driving company"*](https://claude.com/code-with-claude/session/sf-ext-claude-the-self-driving-company):
*"Making company operations fully legible to Claude. Single append-only log of
all unstructured activity (coding sessions, Slack, calls)"* — and Claude
guiding coordination on top of it.

```
Slack Events API ───┐ (signed webhooks)
Linear webhooks ────┤
Granola webhooks ───┼─► Convex HTTP actions ─► events table (append-only) ─┐
POST /ingest ───────┤                                 ▲                    │
agents (log_work) ──┤                     daily cron  │                    │
SessionEnd hook ────┘             Claude coordinator ─┘ (briefing → log,   │
                                                          optional Slack)  ▼
your team's agents ◄── crewlog MCP (stdio, per-user) ◄── whats_next / activity_log / search_log
                                    │
                                    └── live: Linear API (your key), Slack API (your token)
```

The live tools answer "what's next / who's doing what" from the source-of-truth
APIs. The log answers "what happened across the company." Coding sessions land
in it two ways: automatically via the Claude Code `SessionEnd` hook
(`scripts/log-session.ts`), and explicitly via `log_work` (1-3 sentence
summaries agents append mid-flight, from any MCP client). Calls land via
Granola's webhooks. Anything else lands via the generic `/ingest` endpoint.

## Open core

Everything in this repo is MIT. There are two ways to run the log backend, and
the same code serves both:

- **Self-host** — deploy your own Convex project (free tier covers a small
  team; Convex is open-source/self-hostable). Zero extra setup: one team per
  deployment, authenticated by the `CREWLOG_KEY` env var. See
  [Self-hosting the backend](#self-hosting-the-backend-admin-20-minutes).
- **Use a hosted backend, free of charge** — point `CREWLOG_CONVEX_URL` at a
  shared multi-tenant deployment (e.g. the maintainer's — open an issue to get
  a team) and use the team key you're given. Your webhook URLs get a
  `/t/<team>/` prefix; everything else is identical. Note the trust tradeoff:
  the operator of a hosted backend can read your team's log and the
  integration secrets you configure — self-host when that's not acceptable.

Anyone can operate a shared backend for others — see
[Operating a shared backend](#operating-a-shared-backend-multi-tenant).

## Tools

| Tool | Source | What it does |
|---|---|---|
| `whats_next` | Log + Linear | Latest coordinator briefing, then your in-progress issues, queue by priority, urgent unassigned work |
| `team_status` | Linear | Who is working on what right now, plus unassigned urgent/high |
| `get_issue` | Linear | Full issue detail by identifier (ENG-123) incl. comments |
| `search_linear` | Linear | Text search over issues |
| `update_issue` | Linear (write) | Change state / assignee / priority |
| `comment_on_issue` | Linear (write) | Add a markdown comment |
| `create_issue` | Linear (write) | Create an issue in a team |
| `search_slack` | Slack | Search messages with your visibility (`in:#chan from:@user` works) |
| `slack_channel_history` | Slack | Recent messages from one channel |
| `activity_log` | Log | Company-wide feed: Slack + Linear + calls + agent work, one timeline |
| `search_log` | Log | Full-text search over the log (incl. agent work summaries) |
| `log_work` | Log (write) | Append a 1-3 sentence work summary so other agents see it |

Slack is read-only by design. The log is append-only: nothing updates or deletes.

## Auth model

- **Per-user Linear API key** — "my tasks" resolves to the individual, and writes
  are attributed to the person whose agent made them.
- **Per-user Slack user token** (xoxp) — enables real Slack search and respects
  each person's channel visibility. One shared Slack app; each teammate
  authorizes once via `pnpm run slack-auth`.
- **Team-shared log credentials** — a Convex deployment URL and a shared
  secret (the `CREWLOG_KEY` env var when self-hosting; a per-team key, stored
  hashed, on a hosted backend).
- **Server-side secrets** (bot token, webhook secrets) live only in the Convex
  deployment — in its environment when self-hosting, in the team's row on a
  hosted backend — never in the repo or on laptops.

Two consequences worth knowing: all four client env vars are required even if
you only use the log tools (the log resolves your name via Linear so entries
are attributed consistently), and anyone holding the team key can append any
event to the log — including `briefing`-kind events that `whats_next`
surfaces — so treat the key like the shared secret it is. Nothing can ever
modify or delete a logged event. Vulnerability reports: see
[SECURITY.md](SECURITY.md).

## Self-hosting the backend (admin, ~20 minutes)

1. **Clone and install**:

   ```sh
   git clone <your crewlog repo> && cd crewlog
   pnpm install
   ```

2. **Deploy the log backend** ([Convex](https://convex.dev); free tier covers a
   small team, and Convex is open-source/self-hostable):

   ```sh
   npx convex deploy
   ```

   In the Convex dashboard, set env vars on the deployment:
   `SLACK_SIGNING_SECRET`, `SLACK_BOT_TOKEN` (from step 3),
   `LINEAR_WEBHOOK_SECRET` (invent a strong random string), and `CREWLOG_KEY`
   (invent another). Note the deployment's two URLs: `*.convex.cloud` (client)
   and `*.convex.site` (HTTP actions).

3. **Slack app**: create at api.slack.com/apps → "From a manifest" → paste
   `slack-app-manifest.yaml` with the request URL set to
   `https://<deployment>.convex.site/slack/events`. Install to workspace.
   Invite the bot to every channel that should be logged: `/invite @crewlog`.
   Bot membership is the logging opt-in.

4. **Linear webhook**: Linear → Settings → API → Webhooks → new webhook with URL
   `https://<deployment>.convex.site/linear/webhook`, secret =
   `LINEAR_WEBHOOK_SECRET`, event types: Issues + Comments.

5. **Calls via Granola** (optional; needs a [Granola](https://granola.ai)
   Business/Enterprise plan for API access): in the Granola desktop app,
   Settings → Connectors → API keys → create a key (`grn_...`). Register a
   webhook for new meeting notes:

   ```sh
   curl -X POST https://api.granola.ai/v1/webhook-endpoints \
     -H "Authorization: Bearer grn_YOUR_KEY" -H "Content-Type: application/json" \
     -d '{"url": "https://<deployment>.convex.site/granola/webhook",
          "scopes": ["public"], "events": ["note.generated"]}'
   ```

   The response contains a `signing_secret` (`whsec_...`), shown only once. Set
   `GRANOLA_WEBHOOK_SECRET` (that secret) and `GRANOLA_API_KEY` (the `grn_` key)
   on the Convex deployment. Each new meeting summary now lands in the log as a
   `call` event. Other transcription tools can post to `/ingest` instead (below).

6. **Daily coordinator** (optional): set `ANTHROPIC_API_KEY` on the Convex
   deployment. Every day at 13:00 UTC (edit `convex/crons.ts` to change),
   Claude reads the last 24h of the log and appends a briefing that every
   teammate's `whats_next` then surfaces first. Optional extras:
   `CREWLOG_BRIEFING_CHANNEL` (a Slack channel id or `#name` the bot is in —
   posts the briefing there; needs the manifest's `chat:write` scope) and
   `CREWLOG_COORDINATOR_MODEL` (defaults to `claude-opus-5`; the request ships
   with a server-side fallback to `claude-opus-4-8` in case the primary model
   declines — set the env var to a current model when these are eventually
   deprecated). To run without the coordinator, don't set `ANTHROPIC_API_KEY`
   and delete the registration in `convex/crons.ts`.

7. **Distribute** via your secret manager: `CREWLOG_CONVEX_URL` (the
   `*.convex.cloud` URL), `CREWLOG_KEY`, and the Slack app's Client ID + Secret.

## Operating a shared backend (multi-tenant)

One deployment can host the log for many teams free of charge — this is how
the hosted option works. Teams are rows in the `teams` table, created and
configured by the operator with `npx convex run` (never over HTTP):

```sh
# Create a team: mints /t/acme/* webhook routes; the key is what the team's
# MCP clients authenticate with (only its sha256 hash is stored).
npx convex run teams:create '{"slug":"acme","name":"Acme","key":"'"$(openssl rand -hex 32)"'"}'

# Set the team's integration secrets (the team creates its own Slack app /
# Linear webhook / Granola key, using /t/acme/-prefixed request URLs, and
# hands the secrets to the operator):
npx convex run teams:configure '{"slug":"acme","slackSigningSecret":"...","slackBotToken":"xoxb-..."}'
npx convex run teams:configure '{"slug":"acme","linearWebhookSecret":"..."}'
npx convex run teams:configure '{"slug":"acme","granolaApiKey":"grn_...","granolaWebhookSecret":"whsec_..."}'

# Opt the team into the daily coordinator briefing (uses the deployment's
# ANTHROPIC_API_KEY) and optionally post it to their Slack:
npx convex run teams:configure '{"slug":"acme","coordinatorEnabled":true,"briefingChannel":"#standup"}'

# Rotate a team's key:
npx convex run teams:configure '{"slug":"acme","key":"'"$(openssl rand -hex 32)"'"}'
```

The team then uses `CREWLOG_CONVEX_URL` = the shared deployment's
`*.convex.cloud` URL and `CREWLOG_KEY` = their team key. Webhook URLs are the
self-host ones with a `/t/<slug>` prefix, e.g.
`https://<deployment>.convex.site/t/acme/slack/events`. Events are scoped per
team everywhere (indexes and search included). Webhook ingestion is
idempotent — Slack and Granola deliveries dedupe on their event ids, Linear
on a hash of the delivery body — so retries never double-log. One operator
caveat: the log is append-only and never pruned, so storage grows with tenant
activity; a retention lever is deliberately out of scope for now.

### Anything else → POST /ingest

Any other activity source (Zapier zaps, other call-transcription tools, cron
scripts) can append to the log directly:

```sh
curl -X POST https://<deployment>.convex.site/ingest \
  -H "Authorization: Bearer $CREWLOG_KEY" -H "Content-Type: application/json" \
  -d '{"source": "call", "actor": "Fireflies", "kind": "meeting",
       "ref": "Weekly sync", "text": "Discussed the Q3 roadmap...", "url": "https://..."}'
```

`source` must be one of `slack | linear | agent | call`; `ts` (epoch ms) is
optional and defaults to now. Pass an optional `external_id` (any stable
string) to make retries idempotent — a second request with the same
`external_id` is a no-op.

## Per-teammate setup (~5 minutes)

1. Clone crewlog (by convention, next to the repos you work in) and install:

   ```sh
   git clone <your crewlog repo> && pnpm install --dir crewlog
   ```

2. **Linear key**: linear.app → Settings → Security & access → API keys.
3. **Slack token**:

   ```sh
   SLACK_CLIENT_ID=... SLACK_CLIENT_SECRET=... pnpm --dir crewlog run slack-auth
   ```

4. **Export all four vars** in `~/.zshrc` (MCP clients expand `${VAR}` from the
   shell — they do not read `.env` files):

   ```sh
   export LINEAR_API_KEY=lin_api_...
   export SLACK_USER_TOKEN=xoxp-...
   export CREWLOG_CONVEX_URL=https://<deployment>.convex.cloud
   export CREWLOG_KEY=...
   ```

5. Register the server in your client (below), restart it, and ask:
   *"what should I do next?"*

6. **Automatic session logging** (Claude Code): add a `SessionEnd` hook to the
   project repos you work in (commit `.claude/settings.json` once and the whole
   team gets it). When a session ends, `scripts/log-session.ts` digests the
   transcript — first ask, follow-up/tool-call counts, outcome — and appends it
   to the log so every teammate's agent sees what was worked on:

   ```json
   {
     "hooks": {
       "SessionEnd": [
         {
           "hooks": [
             {
               "type": "command",
               "command": "pnpm --dir ../crewlog exec tsx scripts/log-session.ts"
             }
           ]
         }
       ]
     }
   }
   ```

   Adjust `../crewlog` to where crewlog is cloned relative to the project. The
   hook inherits your shell env, so the same four exported vars cover it.

## Client configuration

All examples assume crewlog is cloned at `../crewlog` relative to your project
root — adjust the path to your layout. Commit these to your project repo once
and the whole team gets them.

**Claude Code** — `.mcp.json` at your project root:

```json
{
  "mcpServers": {
    "crewlog": {
      "command": "pnpm",
      "args": ["--dir", "../crewlog", "exec", "tsx", "src/index.ts"],
      "env": {
        "LINEAR_API_KEY": "${LINEAR_API_KEY:-}",
        "SLACK_USER_TOKEN": "${SLACK_USER_TOKEN:-}",
        "CREWLOG_CONVEX_URL": "${CREWLOG_CONVEX_URL:-}",
        "CREWLOG_KEY": "${CREWLOG_KEY:-}"
      }
    }
  }
}
```

**OpenCode** — `opencode.json` at your project root (local servers inherit your
shell env):

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "crewlog": {
      "type": "local",
      "command": ["pnpm", "--dir", "../crewlog", "exec", "tsx", "src/index.ts"],
      "enabled": true
    }
  }
}
```

**Codex** — `~/.codex/config.toml` (Codex's config is global by default;
trusted projects may also use a project-scoped `.codex/config.toml`). Codex
does not expand `${VAR}` in this file, so either rely on it inheriting your
shell env, or set the values explicitly in the server's `env` table:

```toml
[mcp_servers.crewlog]
command = "pnpm"
args = ["--dir", "/path/to/crewlog", "exec", "tsx", "src/index.ts"]

[mcp_servers.crewlog.env]
LINEAR_API_KEY = "lin_api_..."
SLACK_USER_TOKEN = "xoxp-..."
CREWLOG_CONVEX_URL = "https://<deployment>.convex.cloud"
CREWLOG_KEY = "..."
```

## Development

```sh
pnpm run typecheck        # tsc over src/scripts/tests and convex/
pnpm test                 # vitest: format helpers, webhook mappers, crypto vectors
pnpm run inspect          # MCP inspector UI
pnpm exec convex dev      # local Convex backend (anonymous, no login needed)
```

Webhook payload → log event mapping is pure (`convex/mappers.ts`) and covered
by fixture tests; signature verification lives in `convex/lib.ts` with
known-answer vectors. CI runs typecheck + tests on every PR and push to main.

This repo's own `.mcp.json` points at itself, so opening it in Claude Code
dogfoods the server. `convex/_generated/` is committed so the package
typechecks without a Convex login; `convex dev`/`codegen` refreshes it after
schema changes.

## License

MIT

TDQS

A4.1/5.0

Scored across 12 tools

Disambiguation5/5

Each tool targets a distinct purpose: Linear issue operations, Slack queries, activity log actions, and coordination tools. No two tools overlap in functionality.

Naming Consistency5/5

All tool names follow a consistent verb_noun snake_case pattern (e.g., get_issue, search_slack, log_work). Minor abbreviation in 'whats_next' is acceptable.

Tool Count5/5

12 tools is well within the ideal range and covers all necessary actions for team coordination without being excessive.

Completeness4/5

The tool set covers core workflows: issue lifecycle, Slack context, activity logging, and status queries. Minor omissions like issue deletion or Slack messaging are acceptable given the domain scope.

Maintenance

ActivityStale
ResponsivenessNo issues