CLAUDE.md
CLAUDE.mdCLAUDE.mdroot
Quality
77/100
Scores the file, not the repository.Length
5,784 words
87 headings · 17 code blocksRepository
37
— · pushed 0 days agoLast changed
3 days ago
First indexed 3 days ago.1# Claude Code Instructions for claude-threads23## What This Project Does45This is a multi-platform bot that lets users interact with Claude Code through chat platforms. When someone @mentions the bot in a channel, it spawns a Claude Code CLI session in a configured working directory and streams all output to a thread. The user can continue the conversation by replying in the thread.67**Currently Supported Platforms:**8- Mattermost (full support)9- Slack (full support)1011**Key Features:**12- Real-time streaming of Claude responses to chat platforms13- **Multi-platform support** - connect to multiple Mattermost/Slack instances simultaneously14- **Multiple concurrent sessions** - one per thread, across all platforms15- **Session persistence** - sessions resume automatically after bot restart16- **Session collaboration** - `!invite @user` to temporarily allow users in a session17- **Message approval** - unauthorized users can request approval for their messages18- **Thread context prompt** - when starting a session mid-thread, offers to include previous conversation context19- Interactive permission approval via emoji reactions20- Plan approval and question answering via reactions21- Task list display with live updates22- Code diffs and file previews23- Multi-user access control24- Automatic idle session cleanup25- **Permalink follower (`read_post` MCP tool)** - Claude can resolve a Mattermost or Slack permalink to its content (and optional thread context) inside the bot's own channel2627## Contribution Conventions2829- **Pull request titles and bodies are written in English.** This is an open-source project with an international audience, so PRs stay in English even though commit messages may be in Dutch. Keep the CHANGELOG in English as well.3031## Architecture Overview3233```34┌─────────────────────────────────────────────────────────────────┐35│ Chat Platform │36│ ┌──────────────┐ ┌──────────────────────┐ │37│ │ User message │ ───WebSocket───▶ │ PlatformClient │ │38│ │ + reactions │ ◀─────────────── │ (Mattermost/Slack) │ │39│ └──────────────┘ └──────────┬───────────┘ │40└─────────────────────────────────────────────────┼───────────────┘41 │42 ┌───────────────────────────┴────────────────┐43 │ SessionManager │44 │ - Orchestrates session lifecycle │45 │ - Delegates to specialized modules │46 │ - sessions: Map<sessionId, Session> │47 │ - postIndex: Map<postId, threadId> │48 └───────────────────────────┬────────────────┘49 │50 ┌───────────────────┼───────────────────┐51 │ │ │52 ▼ ▼ ▼53 ┌───────────┐ ┌───────────┐ ┌───────────┐54 │ Session │ │ Session │ │ Session │55 │ (thread1) │ │ (thread2) │ │ (thread3) │56 └─────┬─────┘ └─────┬─────┘ └─────┬─────┘57 │ │ │58 ▼ ▼ ▼59 ┌───────────┐ ┌───────────┐ ┌───────────┐60 │ ClaudeCli │ │ ClaudeCli │ │ ClaudeCli │61 │ + MCP srv │ │ + MCP srv │ │ + MCP srv │62 └───────────┘ └───────────┘ └───────────┘63```6465**Session contains:**66- `claude: ClaudeCli` - the Claude CLI process67- `claudeSessionId: string` - UUID for session persistence/resume68- `messageManager: MessageManager` - orchestrates all message operations and state69- `sessionAllowedUsers: Set<string>` - per-session allowlist (includes session owner)70- `isResumed: boolean` - whether session was resumed after restart7172**MessageManager contains executors that own their state:**73- `ContentExecutor` - content streaming state74- `TaskListExecutor` - task list display state75- `QuestionApprovalExecutor` - pending questions/approvals76- `PromptExecutor` - context prompts, worktree prompts, update prompts77- `SubagentExecutor` - active subagent tracking78- `MessageApprovalExecutor` - unauthorized message approval79- `BugReportExecutor` - bug report flow8081**MCP Server:**82- Spawned via `--mcp-config` per Claude CLI instance83- Each has its own WebSocket/connection to the platform84- Exposes three tools to Claude:85 - `permission_prompt` — posts permission requests to the session's thread; returns allow/deny based on user reaction86 - `send_file` — uploads a file from the session's working directory into the thread (auto-approved; path-validated)87 - `read_post` — fetches a Mattermost/Slack post (and optional thread context) by permalink, scoped to the bot's own channel (auto-approved)8889## Multi-Platform Support9091**Architecture**: claude-threads supports connecting to multiple chat platforms simultaneously through a platform abstraction layer.9293**Currently Supported**:94- ✅ Mattermost (fully implemented)95- ✅ Slack (fully implemented)9697**Key Concepts**:98991. **Platform Abstraction**: `PlatformClient` interface normalizes differences between platforms1002. **Composite Session IDs**: Sessions are identified by `"platformId:threadId"` to ensure uniqueness across platforms1013. **Independent Credentials**: Each platform instance has its own URL, token, and channel configuration1024. **Per-Platform MCP Servers**: Each session's MCP permission server connects to the correct platform103104**Configuration**:105106Multi-platform mode uses YAML config (`~/.config/claude-threads/config.yaml`):107108```yaml109version: 1110workingDir: /home/user/repos/myproject111chrome: false112worktreeMode: prompt113respondOnlyWhenMentioned: false # New threads only reply when @mentioned (per-thread !mentions overrides)114userAttribution: true # Prefix user turns with [@username]: so Claude can tell speakers apart (default on; only applied once a thread has >1 participant)115116# Optional: Customize the sticky channel message117stickyMessage:118 description: "Porygon — Mixpanel analytics bot" # Shown below the title119 footer: "• !stop — End session\n• !help — Show help" # Shown before the default footer120121platforms:122 # Mattermost configuration123 - id: mattermost-main124 type: mattermost125 displayName: Main Team126 url: https://chat.example.com127 token: your-bot-token-here128 channelId: abc123129 botName: claude-code130 allowedUsers: [alice, bob]131 skipPermissions: false132133 # Slack configuration134 - id: slack-workspace135 type: slack136 displayName: Slack Team137 botToken: xoxb-your-bot-token # Bot User OAuth Token138 appToken: xapp-your-app-token # App-Level Token (for Socket Mode)139 channelId: C0123456789140 botName: claude-bot141 allowedUsers: [alice, bob] # Slack usernames142 skipPermissions: false143```144145**Slack-specific notes:**146- Requires both a Bot Token (`xoxb-`) and App Token (`xapp-`) for Socket Mode147- `allowedUsers` uses Slack usernames (not user IDs) for consistency with Mattermost148- User mentions in messages use Slack user IDs (e.g., `<@U0123ALICE>`) - the bot handles this automatically149- Bot Token scopes required: `channels:history`, `channels:read`, `chat:write`, `files:read`, `reactions:read`, `reactions:write`, `users:read`150- App Token scope: `connections:write` (Socket Mode must be enabled in the Slack app)151152Configuration is stored in YAML only - no `.env` file support.153154## Multi-Account Claude Support (opt-in)155156By default every session spawns `claude` with the bot's own `process.env`, so157they all share one subscription's token budget. When you expect heavy concurrent158use, configure a pool of accounts in `config.yaml` — new sessions are routed to159whichever account has the most subscription headroom (see **usage balancing**160below) and automatically skip accounts that are in rate-limit cooldown.161162```yaml163# Omit this block entirely → single-account mode (unchanged behavior).164claudeAccounts:165 # OAuth Pro/Max — prepare the HOME with `HOME=<path> claude login` first166 - id: primary167 home: /home/bot/.claude-accounts/primary168 - id: backup169 displayName: Backup (Pro)170 home: /home/bot/.claude-accounts/backup171172 # API-key billed173 - id: shared-api174 apiKey: sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx175```176177How it works:1781791. **Spawn env override.** For `home` we set `HOME` (and `USERPROFILE` on Windows)180 so Claude reads `.credentials.json`, `.claude/projects/*`, and MCP config from181 that directory. For `apiKey` we set `ANTHROPIC_API_KEY` (leaving HOME as-is).1822. **Session → account binding is persisted.** `PersistedSession.claudeAccountId`183 stores which account a session started on, so resume after a bot restart uses184 the same credentials — critical for OAuth accounts since the conversation185 history in `~/.claude/projects/*` lives under that HOME.1863. **Rate-limit handling.** Claude's stderr and result events are scanned for187 phrases like `usage limit reached`, `rate_limit_error`, `429 ... rate limit`,188 `quota exceeded`. On a hit the offending account is cooled down until the189 extracted reset time (fallback: 1 hour). Future `acquireClaudeAccount()` calls190 skip cooling accounts; resumed sessions bypass cooldown because their history191 can't move.1924. **Usage balancing (new sessions).** Instead of round-robin, the pool routes193 each new session to the account with the most subscription headroom. At new194 session start (and only then — there is no background polling) the bot probes195 every account **on-demand and in parallel** with `claude -p "/usage"196 --output-format json` under each account's HOME (costs $0, zero turns) and197 parses the real limit percentages (`Current session` + `Current week`). The198 load score is `max(session%, week%)`; `acquire()` picks the lowest199 non-cooling score, tie-breaking by fewest active sessions then config order.200 Each probe is capped at 10s; an account whose probe fails/times out sorts201 last (usage "unknown"), so a fresh session is never routed onto a202 possibly-maxed account before its real usage is known. The sticky channel203 message shows the pool's `min–max % used` range.204205 > On-demand probing adds ~1–2s to new session start (all accounts are probed206 > so they can be compared) — negligible next to spawning Claude + the MCP207 > server, and it keeps routing data always-fresh with zero idle work. Probing208 > no-ops for pools with fewer than two accounts.209 >210 > Usage balancing targets **OAuth (subscription)** accounts — only they211 > report `/usage` limits. API-key accounts return no percentages; they sort212 > as "usage unknown" and are picked by the active-session tiebreak. Usage is213 > cached in memory only for the current pool state; it is re-probed fresh at214 > each new session start.215216 **Resume is unaffected:** it still passes the persisted `claudeAccountId` as217 `preferredId`, so a resumed session always re-binds to the account its218 history lives under, cooling or not.219220Files involved:221222| File | Role |223|------|------|224| `src/claude/account-pool.ts` | `AccountPool` — usage-balanced selection, cooldown tracking, active-session accounting. |225| `src/claude/usage-probe.ts` | Runs `claude -p "/usage"` per account + pure `parseUsageOutput` / `usageLoadScore`. |226| `src/session/manager.ts` | `refreshAccountUsage()` probes all accounts on-demand at session start; exposed to lifecycle as `ops.refreshClaudeAccountUsage()`. |227| `src/claude/rate-limit-detector.ts` | Pure parser that turns stderr/JSON into `{ detected, resetAtEpochMs }`. |228| `src/claude/cli.ts` | `ClaudeCliOptions.account` → overrides `HOME` / `ANTHROPIC_API_KEY` on spawn. Emits `rate-limit` events. |229| `src/session/lifecycle.ts` | Acquires the account on `startSession`/`resumeSession`, releases on `removeFromRegistry`, handles `rate-limit` events. |230| `src/operations/commands/handler.ts` | Preserves the account when `!cd` / `!permissions interactive` respawn Claude; adds the 🔑 row to the session header. |231| `src/operations/sticky-message/handler.ts` | Pool summary in the channel sticky. |232233## Source Files234235### Core236| File | Purpose |237|------|---------|238| `src/index.ts` | Entry point. CLI parsing, bot startup, UI rendering |239| `src/message-handler.ts` | Message routing logic (extracted for testability) |240| `src/config.ts` | Type exports for config (re-exports from migration.ts) |241| `src/config/migration.ts` | YAML config loading (`config.yaml`) |242| `src/onboarding.ts` | Interactive setup wizard for multi-platform config |243244### Session Management245246Session is a thin container; most logic lives in `src/operations/`:247248| File | Purpose |249|------|---------|250| `src/session/manager.ts` | **Orchestrator** - creates sessions, routes messages/reactions |251| `src/session/reaction-router.ts` | Reaction dispatch: allowlist gate, resume-from-reaction, session-level reactions, MessageManager fallthrough |252| `src/session/lifecycle.ts` | Session start, resume, exit, cleanup |253| `src/session/types.ts` | TypeScript types (Session interface) |254| `src/session/registry.ts` | Session lookup and registration |255| `src/session/timer-manager.ts` | Per-session timer management |256| `src/session/index.ts` | Public exports |257258### Operations Layer (The Brain)259260Most business logic lives in `src/operations/`:261262| File | Purpose |263|------|---------|264| `src/operations/message-manager.ts` | **The Brain** - orchestrates all operations via executors |265| `src/operations/transformer.ts` | Transforms Claude events → MessageOperations |266| `src/operations/types.ts` | Operation types (MessageOperation, TaskItem, etc.) |267| `src/operations/post-helpers/` | DRY utilities for posting messages (postInfo, postError, etc.) |268| `src/operations/events/handler.ts` | Claude CLI event handling |269| `src/operations/commands/handler.ts` | User commands (!cd, !invite, !kick, !permissions) |270| `src/operations/streaming/handler.ts` | Message batching and flushing to chat |271| `src/operations/context-prompt/handler.ts` | Thread context prompt for mid-thread session starts |272| `src/operations/worktree/handler.ts` | Git worktree management |273| `src/operations/bug-report/handler.ts` | Bug report flow |274| `src/operations/sticky-message/handler.ts` | Channel sticky message |275| `src/operations/tool-formatters/` | Format tool use for display |276277### Executors (State Owners)278279Each executor owns a specific piece of interactive state:280281| File | Purpose |282|------|---------|283| `src/operations/executors/content.ts` | Content streaming state |284| `src/operations/executors/task-list.ts` | Task list display |285| `src/operations/executors/question-approval.ts` | Questions and plan approval |286| `src/operations/executors/prompt.ts` | Context prompts, worktree prompts, update prompts |287| `src/operations/executors/subagent.ts` | Subagent tracking |288| `src/operations/executors/message-approval.ts` | Unauthorized message approval |289| `src/operations/executors/bug-report.ts` | Bug report flow |290| `src/operations/executors/system.ts` | System messages |291| `src/operations/executors/worktree-prompt.ts` | Worktree prompts |292293**Design Pattern**: MessageManager delegates to executors. Each executor owns its state and handles its reactions. This keeps Session minimal while centralizing all message operations.294295### Claude CLI296| File | Purpose |297|------|---------|298| `src/claude/cli.ts` | Spawns Claude CLI with platform-specific MCP config |299| `src/claude/types.ts` | TypeScript types for Claude stream-json events |300| `src/claude/version-check.ts` | Claude CLI version validation and compatibility check |301302### Platform Layer303| File | Purpose |304|------|---------|305| `src/platform/client.ts` | PlatformClient interface (abstraction for all platforms) |306| `src/platform/types.ts` | Normalized types (PlatformPost, PlatformUser, PlatformReaction, etc.) |307| `src/platform/formatter.ts` | PlatformFormatter interface (markdown dialects) |308| `src/platform/utils.ts` | **NEW** - Platform-agnostic utilities (message splitting, emoji normalization, retry logic) |309| `src/platform/IMPLEMENTATION_GUIDE.md` | **NEW** - Guide for implementing new platform support |310| `src/platform/index.ts` | Public exports |311| `src/platform/mattermost/client.ts` | Mattermost implementation of PlatformClient |312| `src/platform/mattermost/types.ts` | Mattermost-specific types |313| `src/platform/mattermost/formatter.ts` | Mattermost markdown formatter |314| `src/platform/mattermost/permalink.ts` | Mattermost permalink parser + resolver + formatter for `read_post` |315| `src/platform/slack/client.ts` | Slack implementation of PlatformClient (Socket Mode + Web API) |316| `src/platform/slack/types.ts` | Slack-specific types |317| `src/platform/slack/formatter.ts` | Slack mrkdwn formatter |318| `src/platform/slack/mcp-platform-api.ts` | Slack MCP platform API (used by MCP child) |319| `src/platform/slack/permalink.ts` | Slack permalink parser + resolver + formatter for `read_post` |320| `src/platform/slack/index.ts` | Slack module exports |321| `src/platform/permalink-shared.ts` | Cross-platform permalink utilities (caps, truncation, quote-block) shared by both permalink modules |322| `src/platform/test-helpers/fetch-harness.ts` | Shared `fetch` recorder + responder for platform-API unit tests |323324### Utilities325| File | Purpose |326|------|---------|327| `src/utils/emoji.ts` | Emoji constants and validators (platform-agnostic) |328| `src/utils/logger.ts` | Component-based logging with session context |329| `src/utils/session-log.ts` | Session-aware logging utilities |330| `src/utils/format.ts` | ID formatting, time/duration formatting |331| `src/utils/colors.ts` | Terminal color utilities |332| `src/utils/keep-alive.ts` | Prevent system sleep during sessions |333| `src/utils/battery.ts` | Battery status monitoring |334| `src/utils/uptime.ts` | Session uptime tracking |335| `src/utils/pr-detector.ts` | Detect PR URLs in Claude output |336| `src/mcp/mcp-server.ts` | MCP server: permission prompts, send_file, read_post (platform-agnostic) |337| `src/platform/mcp-platform-api-factory.ts` | Factory for platform-specific MCP platform APIs |338| `src/platform/mcp-platform-api.ts` | McpPlatformApi interface |339| `src/mattermost/api.ts` | Standalone Mattermost API helpers |340| `src/persistence/session-store.ts` | Multi-platform session persistence |341| `src/ui/components/Header.tsx` | Terminal header with the ASCII logo |342343## How the Permission System Works3443451. **Claude CLI is started with:**346```347 claude --input-format stream-json --output-format stream-json --verbose \348 --mcp-config '{"mcpServers":{"claude-threads-mcp":{...}}}' \349 --permission-prompt-tool mcp__claude-threads-mcp__permission_prompt350```3513522. **When Claude needs permission** (e.g., to write a file), it calls the MCP tool3533543. **The MCP server** (running as a subprocess):355 - Receives the permission request via stdio356 - Posts a message to the chat thread: "⚠️ Permission requested: Write `file.txt`"357 - Adds reaction options (👍 ✅ 👎) to the message358 - Opens a WebSocket to the platform and waits for a reaction3593604. **User reacts** with an emoji3613625. **MCP server**:363 - Validates the user is in ALLOWED_USERS364 - Ignores bot's own reactions (the reaction options)365 - Returns `{behavior: "allow"}` or `{behavior: "deny"}` to Claude CLI3663676. **Claude CLI** proceeds or aborts based on the response368369## Configuration370371Configuration is stored in YAML at `~/.config/claude-threads/config.yaml`.372373**First run:** If no config exists, interactive onboarding guides you through setup.374375### Environment Variables (Optional)376377| Variable | Description |378|----------|-------------|379| `MAX_SESSIONS` | Max concurrent sessions (default: `5`) |380| `SESSION_TIMEOUT_MS` | Idle session timeout in ms (default: `1800000` = 30 min) |381| `DEBUG` | Set `1` for debug logging |382| `CLAUDE_PATH` | Custom path to claude binary (default: `claude`) |383| `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` | Set `1` to have Claude strip Anthropic/cloud credentials (`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_BEARER_TOKEN_BEDROCK`, `GOOGLE_APPLICATION_CREDENTIALS`) from Bash, hook, and stdio-MCP subprocesses it spawns. Bot env vars like `PLATFORM_TOKEN` / `MATTERMOST_TOKEN` / `SLACK_BOT_TOKEN` pass through untouched — verified empirically against CLI 2.1.116. **Side effect:** setting this also forces permission mode to `default` — Claude will refuse `--dangerously-skip-permissions` and log a warning. Only enable if all your sessions run with interactive permissions. Requires Claude CLI 2.1.83+. |384385The bot also sets two Claude CLI tuning flags by default on the child process,386and only if you haven't already set them in the parent env:387388- `MCP_CONNECTION_NONBLOCKING=true` — caps `--mcp-config` server connects at 5s389 so a slow MCP server never blocks session start (Claude CLI 2.1.89+).390- `ENABLE_PROMPT_CACHING_1H=true` — opts into the 1-hour prompt cache TTL,391 reducing re-caching cost on long-lived threads (Claude CLI 2.1.108+).392393Export either with a non-default value (e.g. `MCP_CONNECTION_NONBLOCKING=false`)394to disable.395396### Lockfiles: why there are two, and the trap397398The repo carries **both** `bun.lock` and `package-lock.json`, and they do399different jobs:400401| File | Role |402|------|------|403| `bun.lock` | What CI actually installs from. Every workflow runs `bun install`. This is the source of truth for what gets tested, built and published. |404| `package-lock.json` | Installed by nothing. It exists so Dependabot **security** updates keep working: Dependabot supports bun for *version* updates only, so with bun.lock alone you lose security-advisory PRs. Also a second Trivy scan surface. |405406**The trap:** Dependabot only ever rewrites `package-lock.json`. A dependency407bump therefore lands, goes green, and never reaches CI — because the version CI408installs still comes from `bun.lock`. This is not hypothetical; #434 and #442409both needed `bun.lock` regenerated by hand before the bump took effect.410`.github/workflows/dependabot-sync-lockfile.yml` now does that automatically on411Dependabot's PRs.412413Note that the two lockfiles **cannot be kept fully identical** — npm and bun hoist414transitive trees differently, and both results are valid, so dozens of415transitive versions legitimately differ. Only *direct* dependencies resolving416differently indicates a real problem. Don't try to enforce byte-level parity.417418**Longer-term option:** [Renovate](https://docs.renovatebot.com/modules/manager/bun/)419supports bun natively, including `vulnerabilityAlerts` for security-driven PRs.420Switching to it would let the repo drop `package-lock.json` entirely and remove421this whole class of drift. That is a deliberate change of dependency bot, not422something to do incidentally.423424### Runtime Version Policy425426**Floor**: Node 20 (in maintenance LTS through April 2026), Bun 1.2.21.427Both are declared in `package.json#engines`.428429**Why Node 20 and not higher**: no production dep requires more, and bumping430the floor strands users on otherwise-supported LTS lines. Forced to 20 by431`@hono/node-server@2` in v1.8.2.432433**CI strategy**:434- Bun is pinned (`BUN_VERSION` env in every workflow) so a Bun release can't435 silently break us. Bump in lockstep when upgrading.436- `publish.yml` builds under Node 20 (the floor) so any unsafe API call that437 only exists on newer Node breaks the build before reaching users.438- `ci.yml` has a `node-smoke` matrix (`[20, 22, 24]`) that runs the built439 binary under each currently-relevant Node line.440441**When to bump the floor**: only when a real dep forces it. Update442`package.json#engines.node`, `publish.yml` Node version, the `node-smoke`443matrix floor, the README prereqs line, and call it out as breaking in the444CHANGELOG.445446### Claude CLI Version Requirements447448claude-threads requires a compatible version of the Claude CLI (`@anthropic-ai/claude-code`).449450**Compatible versions:** `>=2.0.74 <2.2.0` (covers the full 2.1.x line; latest verified: 2.1.116)451452The version is checked at startup. If an incompatible version is detected:453- The bot will display an error message and exit454- Use `--skip-version-check` to bypass (not recommended)455456To install the latest verified compatible version:457```bash458npm install -g @anthropic-ai/claude-code@2.1.116459```460461The Claude CLI version is displayed:462- At bot startup in the terminal463- In the sticky channel message status bar464- In each session's header table465466**Updating the version range:** Edit `CLAUDE_CLI_VERSION_RANGE` in `src/claude/version-check.ts` when testing with new Claude CLI versions.467468## Development Commands469470```bash471bun install # Install dependencies472bun run build # Compile TypeScript to dist/473bun run dev # Run from source with watch mode474bun start # Run compiled version475bun test # Run unit tests (~2600 tests)476bun run lint # Run ESLint477```478479### Integration Tests480481Integration tests run the actual bot against a real Mattermost instance with a mock Claude CLI.482483```bash484# Run locally (requires Docker)485bun run test:integration:setup # Start Mattermost in Docker + create users/channels486bun run test:integration:run # Run ~120 integration tests487bun run test:integration:teardown # Stop Mattermost488489# Or run all at once (CI style)490bun run test:integration491```492493**What's tested:**494- Session lifecycle (start, response, end, timeout)495- Commands (!stop, !escape, !help, !cd, !kill, !permissions)496- Reaction-based controls (❌ cancel, ⏸️ interrupt)497- Multi-user collaboration (!invite, !kick, message approval)498- Session persistence and resume499- Plan approval, question flows, context prompts500- Git worktree integration501- Error handling, MAX_SESSIONS limits502503**CI:** Integration tests run automatically on PRs via `.github/workflows/integration.yml`504505## Testing Locally5065071. Create config: `~/.config/claude-threads/config.yaml` (or run `claude-threads` for interactive setup)5082. Build: `bun run build`5093. Run: `bun start` (or `DEBUG=1 bun start` for verbose output)5104. In Mattermost, @mention the bot: `@botname write "hello" to test.txt`5115. Watch the permission prompt appear, react with 👍5126. Verify file was created513514## Publishing a New Version515516Releases are automated via GitHub Actions. There are two paths, and both end in an517npm publish:5185191. **Version bump lands on `main` → `.github/workflows/release.yml`** verifies the520 tree, creates the tag, creates the GitHub release, and publishes. This needs no521 local machine and no `gh` CLI — merging the version-bump commit is enough.5222. **You create a release by hand with `gh release create`** → the existing523 `.github/workflows/publish.yml` fires on `release: published` and publishes.524525### Automated Release Flow (no local machine needed)526527Everything here can be done from the GitHub web UI or by an agent that can only528push branches and merge PRs:529530```5311. Open a PR that:532 a. Bumps the version in package.json with the semver level the change533 warrants — fixes only → `npm version patch`, a new feature → `npm version534 minor`, a breaking change → `npm version major` (all with535 --no-git-tag-version). `npm version` also updates package-lock.json; run536 `bun install` afterwards so bun.lock stays in lockstep (bun.lock does not537 record the root version, so it is usually a no-op — see the lockfile note538 above).539 b. Promotes the CHANGELOG's `## [Unreleased]` heading to540 `## [X.Y.Z] - YYYY-MM-DD` (the new version and today's date), so the541 release notes match the tag.5422. Merge it to main once CI is green.5433. release.yml picks up the package.json change, re-runs typecheck/lint/knip/544 tests/build, then tags, releases and publishes.545```546547The bump must land as a **new** version: release.yml exits without publishing if548the current version's tag already exists, so re-using a version that was already549released (e.g. leaving package.json unchanged) is a silent no-op, not a release.550551`release.yml` is safe to re-run and safe against unrelated `package.json` edits:552if the tag for the current version already exists it exits before tagging or553publishing. It also exposes `workflow_dispatch`, so a release can be kicked off554from the Actions tab (or the API) if the push trigger was missed — for example555when the version bump reached `main` before the workflow existed.556557> **Why `release.yml` publishes directly instead of handing off to `publish.yml`:**558> a release created with `GITHUB_TOKEN` does not emit a `release: published` event559> that can start another workflow — GitHub suppresses those to avoid recursive560> runs. A tag-and-release-only job would therefore create the release and then561> never publish. The alternative is a long-lived PAT; publishing in-job avoids562> that credential entirely.563564### Quick Release Flow (with open PRs)565566When there are open PRs to merge before releasing:567568```bash569# 1. List open PRs and verify checks pass before merging570gh pr list --state open571gh pr checks <PR_NUMBER> # Ensure all checks pass!572573# 2. Merge PRs (squash merge, delete branches)574gh pr merge <PR_NUMBER> --squash --delete-branch575# Repeat for each PR (ignore worktree branch deletion errors)576577# 3. Pull merged changes578git pull579580# 4. Remove any deprecated files if needed581rm <file> && git add -A582583# 5. Update CHANGELOG.md with new version and all merged PR changes584# Use format: **Feature/Fix name** - Description (#PR_NUMBER)585586# 6. Commit changelog587git add CHANGELOG.md && git commit -m "Update CHANGELOG for vX.Y.Z"588589# 7. Bump version590npm version patch --no-git-tag-version # 0.47.0 → 0.47.1 (fixes only)591npm version minor --no-git-tag-version # 0.47.0 → 0.48.0 (new features)592npm version major --no-git-tag-version # 0.47.0 → 1.0.0 (breaking changes)593594# 8. Commit and tag595git add package.json package-lock.json && git commit -m "X.Y.Z" && git tag vX.Y.Z596597# 9. Push to GitHub with tags598git push && git push --tags599600# 10. Create GitHub release (triggers automatic npm publish)601gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes602```603604### Manual Release Flow (no PRs to merge)605606**IMPORTANT: Always test locally before pushing!**607```bash608# 0. Build and run locally to test609bun run build && bun start610# Test in Mattermost: https://digilab.overheid.nl/chat/digilab/channels/annes-claude-code-sessies611# Kill the server when done testing (Ctrl+C)612```613614```bash615# 1. Update CHANGELOG.md with the new version616617# 2. Commit the changelog618git add CHANGELOG.md && git commit -m "Update CHANGELOG for vX.Y.Z"619620# 3. Bump version621npm version patch --no-git-tag-version # then commit and tag manually622623# 4. Commit and tag624git add package.json package-lock.json && git commit -m "X.Y.Z" && git tag vX.Y.Z625626# 5. Push to GitHub with tags627git push && git push --tags628629# 6. Create GitHub release (this triggers automatic npm publish)630gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes631```632633**GitHub Actions Workflow:** `.github/workflows/publish.yml`634- Triggered on: GitHub release published635- Builds TypeScript and publishes to the npm registry636- Requires `NPM_TOKEN` secret in repository settings637638**⚠️ IMPORTANT: NEVER modify `publish.yml`'s trigger!**639- The workflow MUST trigger on `release: types: [published]`640- NEVER change it to trigger on tag pushes641- This is the path for a release a human creates via `gh release create`642- This is the preferred manual release workflow - do not change it643- Automating a release is done in `release.yml` instead, which is a *separate*644 workflow — never by retargeting this one645646**Both workflows publish, but they cannot double-publish silently:** `release.yml`647skips entirely when the current version's tag already exists, and npm refuses to648republish a version that is already on the registry, so a redundant run fails649loudly rather than shipping anything unexpected.650651**Token Setup (already configured):**652- Classic Automation token stored in GitHub repository secrets as `NPM_TOKEN`653- To update: https://github.com/anneschuth/claude-threads/settings/secrets/actions654655## Testing Deployed Versions in Mattermost656657After deploying a new version, test it in the Mattermost channel:658https://digilab.overheid.nl/chat/digilab/channels/annes-claude-code-sessies659660### Basic Verification6611. **Check version**: `@minion-of-anne what version are you running?`662 - Bot should respond with version number and summary of recent changes663 - Verify the session header shows correct version664665### Testing Permission System6661. **Start a new session** (existing sessions keep their original permission mode)6672. **Enable interactive permissions**: `!permissions interactive`668 - Should see: "🔐 **Interactive permissions enabled** ... *Claude Code restarted with permission prompts*"669 - Session header should update to show "Permissions: Interactive"6703. **Test permission prompt**: `@minion-of-anne write "test" to /tmp/perm-test.txt`671 - Should see a permission prompt with reaction options: 👍 ✅ 👎672 - React with 👍 to approve673 - File should be written after approval674675### Testing Other Features676- **Session collaboration**: `!invite @username` / `!kick @username`677- **Directory change**: `!cd /some/path` (restarts Claude CLI)678- **Interrupt**: `!escape` or ⏸️ reaction (interrupts without killing)679- **Cancel**: `!stop` or ❌/🛑 reaction (kills the session)680- **Plan approval**: When Claude presents a plan, react with 👍/👎681- **Question answering**: When Claude asks questions, react with number emojis682683### Verifying Specific Bug Fixes684When testing a specific fix:6851. Reproduce the original bug scenario6862. Verify the fix works as expected6873. Check for regressions in related functionality688689## Data Retention & Security690691claude-threads stores sensitive session data locally. The following retention policies and security measures apply:692693### Data Storage Locations694695| Data | Location | Retention | Permissions |696|------|----------|-----------|-------------|697| Session state | `~/.config/claude-threads/sessions.json` | Active + 3 days after soft-delete | `0600` (owner only) |698| Thread logs | `~/.claude-threads/logs/{platformId}/` | 30 days (configurable) | `0600` (owner only) |699| Worktree metadata | `~/.claude-threads/worktrees.json` | Until worktree cleanup | `0600` (owner only) |700| Configuration | `~/.config/claude-threads/config.yaml` | Permanent | `0600` (owner only) |701702### Automatic Cleanup703704- **Session purge**: Inactive sessions are soft-deleted after session timeout, then permanently removed after 3 days705- **Thread logs**: Automatically deleted after 30 days (configurable via `threadLogs.retentionDays` in config)706- **Worktrees**: Orphaned worktrees (no active session, >24h old) are cleaned up automatically707- **Cleanup scheduler**: Runs hourly in the background708709### Security Measures710711- All sensitive files use restrictive permissions (`0600` - owner read/write only)712- Session tokens and credentials are never written to disk (config file excluded)713- Permission decisions are logged for audit purposes714- Bot tokens should be stored securely (environment variables or secure config)715716## Common Issues & Solutions717718### "Permission server not responding"719- Check that `MATTERMOST_URL` and `MATTERMOST_TOKEN` are passed to MCP server720- Look for `[MCP]` prefixed logs in stderr721- Enable `DEBUG=1` for verbose MCP logging722723### "Reaction not detected"724- The MCP server has its own WebSocket connection (separate from main bot)725- Check that the reacting user is in `ALLOWED_USERS`726- Bot's own reactions (adding the 👍 ✅ 👎 options) are filtered out727728### "Claude CLI not found"729- Ensure `claude` is in PATH, or set `CLAUDE_PATH` environment variable730- The CLI must support `--permission-prompt-tool` (recent versions)731732### "MCP config schema error"733- The config must be wrapped: `{"mcpServers": {"name": {"type": "stdio", ...}}}`734- Check `src/claude/cli.ts` for the exact format735736### "TypeScript build errors"737- Run `bun install` to ensure dependencies are up to date738- Check for type mismatches in event handling739740## Debugging with Claude Code History741742Claude Code stores all conversation history on disk, which is invaluable for debugging:743744```745~/.claude/746├── history.jsonl # Index of all sessions (metadata only)747├── projects/ # Full conversation transcripts748│ └── -Users-username-project/ # Encoded path (/ → -)749│ ├── session-id-1.jsonl # Full conversation750│ └── session-id-2.jsonl751├── todos/ # Todo lists per session752└── settings.json # User settings753```754755**Useful debugging commands:**756```bash757# List recent sessions758tail -20 ~/.claude/history.jsonl | jq -r '.cwd + " " + .name'759760# Find sessions for this project761ls ~/.claude/projects/-Users-anneschuth-mattermost-claude-code/762763# View a specific session's conversation764cat ~/.claude/projects/-Users-anneschuth-mattermost-claude-code/SESSION_ID.jsonl | jq .765```766767**Key points:**768- Directory names are encoded: `/path/to/project/` → `-path-to-project`769- Each session gets a JSONL file with full conversation history770- Consider backing up `~/.claude/` regularly771772## Key Implementation Details773774### Event Flow (src/operations/transformer.ts → MessageManager)775Claude CLI emits JSON events. The transformer converts them to MessageOperations:776- `assistant` → `AppendContentOp` (text response)777- `tool_use` → `AppendContentOp` (tool display) or special ops (TaskListOp, QuestionOp, etc.)778- `tool_result` → `AppendContentOp` (result indicator) + `FlushOp`779- `result` → `FlushOp` + `StatusUpdateOp` (cost info)780781### Message Streaming (src/operations/streaming/handler.ts)782- Messages are batched and flushed periodically via `FlushOp`783- Long content is split across multiple posts (16K limit)784- Diffs and code blocks use syntax highlighting785786### Reaction Handling (MessageManager → Executors)787- `MessageManager.handleReaction()` routes to appropriate executor788- Each executor handles its own pending state (questions, approvals, prompts)789- MCP server handles: permission prompts (separate from main bot)790- All filter to only process allowed users' reactions791792## Backward Compatibility Requirements793794**CRITICAL:** When modifying persisted data structures (`PersistedSession`, `config.yaml`, `sessions.json`), you MUST maintain backward compatibility. Users may upgrade from any older version, and their persisted data must continue to work.795796### Rules for Persisted Data Changes7977981. **Never remove fields** - Old data may have them, and removing causes silent failures7992. **Never rename fields without migration** - Add migration logic in `session-store.ts` to convert old field names8003. **Always use defensive defaults** - When reading persisted data, use `??` or `||` to provide fallbacks:801```typescript802 // GOOD - handles missing fields gracefully803 sessionNumber: state.sessionNumber ?? 1,804 sessionAllowedUsers: new Set(state.sessionAllowedUsers || [state.startedBy].filter(Boolean)),805806 // BAD - crashes if field is missing807 sessionNumber: state.sessionNumber,808 sessionAllowedUsers: new Set(state.sessionAllowedUsers),809```8104. **Check both old and new field names** when looking up data:811```typescript812 // GOOD - supports both old and new field names813 const lifecycleId = session.lifecyclePostId || (session as LegacySession).timeoutPostId;814```8155. **Add migrations for field renames** - See `session-store.ts` for examples of migrating `timeoutPostId` → `lifecyclePostId`816817### Testing Backward Compatibility818819When making changes to persisted data:8201. Create a test session with the OLD code8212. Upgrade to the NEW code8223. Verify the session resumes correctly8234. Verify all features work (tasks, permissions, worktrees, etc.)824825### Red-Green Testing for Regression Fixes826827> **CRITICAL: Always verify your test is RED without the fix!**828>829> A test that passes regardless of whether the fix exists is USELESS.830> It won't catch future regressions. This is the most common testing mistake.831832**The RED-GREEN-REFACTOR cycle:**833834| Step | Action | Verification |835|------|--------|--------------|836| 1. **RED** | Write test, temporarily remove fix | Test FAILS |837| 2. **GREEN** | Apply the fix | Test PASSES |838| 3. **REFACTOR** | Clean up code | All tests still pass |839840**The #1 Rule:**841```842TEST THE ACTUAL CODE PATH, NOT A COPY OF THE LOGIC!843```844845- **WRONG**: Test duplicates the if/then logic inline in the test846 - Test passes even if someone deletes the fix847 - Useless for catching regressions848849- **RIGHT**: Test calls the actual function that contains the fix850 - Test fails if fix is removed851 - Actually protects against regressions852853**How to verify your test is RED:**854```bash855# 1. Temporarily comment out or revert the fix856git diff src/path/to/file.ts # Note what you're removing857858# 2. Run JUST your test - it MUST fail859bun test --test-name-pattern "your test name"860861# 3. If test passes → YOUR TEST IS BROKEN, rewrite it!862# If test fails → Good! Now restore the fix.863864# 4. Run test again with fix - should pass865bun test --test-name-pattern "your test name"866867# 5. Run all tests868bun test869```870871**Key principles:**872- Test the specific function/method that contains the fix873- If code is hard to test, refactor for testability FIRST874- A regression test MUST fail if someone removes the fix875- "Documents behavior" is NOT the same as "tests the code"876877### Files That Store Persisted Data878879- `~/.config/claude-threads/sessions.json` - Session state (`PersistedSession` interface)880- `~/.config/claude-threads/config.yaml` - Bot configuration881882## Future Improvements to Consider883884- [x] Implement Slack platform support - **Done**885- [ ] Add rate limiting for API calls886- [x] Support file uploads via chat attachments - **Done**887- [x] Support multiple concurrent sessions (different threads) - **Done in v0.3.0**888- [x] Add `!stop` command to abort running session - **Done in v0.3.4** (also ❌/🛑 reactions)889- [x] CLI arguments and interactive onboarding - **Done in v0.4.0**890- [x] Session collaboration (`!invite`, `!kick`, message approval) - **Done in v0.5.0**891- [x] Persist session state for recovery after restart - **Done in v0.9.0**892- [x] Add `!escape` command to interrupt without killing session - **Done in v0.10.0** (also ⏸️ reaction)893- [x] Add `!kill` command to emergency shutdown all sessions and exit - **Done in v0.10.0**894- [x] Multi-platform architecture - **Done in v0.14.0**895- [x] Modular session management - **Done in v0.14.0**896- [ ] Add rate limiting for API calls897- [ ] Support file uploads via Mattermost attachments898- [ ] Keep task list at the bottommost message (always update to latest position)899- [ ] Session restart improvements: verify all important state is preserved (cwd ✓, permissions ✓, worktree ✓)900- [x] Accurate context usage: Use per-request `usage` field from result events instead of cumulative billing tokens - **Done**901
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.4k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 3 days ago | |
| Adit-Jain-srm/NightmareNetCLAUDE.md · 45 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 3 days ago | |
| microsoft/playwrightCLAUDE.md · 94k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 3 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 3 days ago | |
| dotCMS/coreCLAUDE.md · 949 | CLAUDE.md | setupbuildteststyle+7 | 99/100 | today | |
| lollipopkit/flutter_server_boxCLAUDE.md · 8.3k | CLAUDE.md | buildteststylearch+2 | 98/100 | 3 days ago | |
| carrot-foundation/middle-earthCLAUDE.md · 0 | CLAUDE.md | setupbuildtestlint-format+6 | 97/100 | 3 days ago | |
| caliber-ai-org/ai-setupCLAUDE.md · 1.2k | CLAUDE.md | buildtestlint-formatstyle+1 | 97/100 | 3 days ago |
