Skip to content

onsen-ai/claudia

Repository files navigation

🤖 Claudia

Your team's AI assistant, right inside Slack.

Claudia brings the power of Claude into your Slack workspace. Mention her in any channel, ask a question, give a task — and she gets to work. She can read files, search the web, query databases, run code, and more. No context switching. No copy-pasting between tabs. Just ask.

@claudia what were our top 3 support issues last week?

She'll research, analyze, and reply — all in the thread, in real time.

📑 Table of Contents


✨ What Can Claudia Do?

Claudia isn't a chatbot that just answers questions. She's an agent — she can use tools, take actions, and work through multi-step tasks:

Use case Example
🔍 Research & analysis "Summarize the key points from this document"
📊 Data queries "How many users signed up last month?" (with database MCP tools)
💻 Code tasks "Find all TODO comments in the repo and create a summary"
🌐 Web lookups "What's the latest pricing for AWS Lambda?"
✍️ Content drafting "Write release notes for v2.3 based on the recent commits"
🔧 DevOps help "Check if the staging deploy succeeded and show me the logs"

🧩 Extensible with MCP

Connect Claudia to your own tools and services using Model Context Protocol (MCP) servers. Query your production database, pull from your internal APIs, check GitHub PRs — anything you can build an MCP server for, Claudia can use.


🎯 Why Claudia?

Without Claudia With Claudia
💬 Getting answers Switch to browser, search docs, copy-paste back to Slack Ask in Slack, get the answer in the thread
📈 Data questions Open dashboard, write query, screenshot results "@claudia how many orders shipped this week?"
🔄 Multi-step tasks Manually check 5 different sources, compile a summary Claudia checks all sources and summarizes for you
👥 Team collaboration One person does the research, others wait Everyone asks in the thread, Claudia remembers the full context

🚀 Quick Start

Getting Claudia running takes about 15 minutes. You'll set up 3 things: a Slack app (so Claudia can live in your workspace), AWS credentials (so she has somewhere to run), and an LLM provider — Anthropic API key, Amazon Bedrock (uses your AWS account), Google Gemini, or OpenAI.

📋 Step 0: Prerequisites

Platform: The setup script and local tooling require macOS or Linux. Windows is not supported.

Node.js 22+ — JavaScript runtime

Check if you have it: node --version. If not, install from nodejs.org or use nvm:

nvm install 22
nvm use 22
AWS CLI — for deploying to AWS

Claudia runs as an AWS Lambda function. You need the AWS CLI configured with credentials that can create Lambda functions, S3 buckets, and Secrets Manager secrets.

  1. Install from aws.amazon.com/cli
  2. Run aws configure and enter your Access Key ID, Secret Access Key, and region
  3. Verify with aws sts get-caller-identity — you should see your account info
Docker Desktop — builds the Lambda container image

Claudia deploys as a Docker container image on Lambda. Install Docker Desktop and make sure it's running before deploying.

⚠️ Containerd image store: If your Docker Desktop has "Use containerd for pulling and storing images" enabled (Settings → General), deployments will fail with image manifest ... is not supported. This is because the containerd backend produces manifest lists (OCI index) that Lambda doesn't accept. Uncheck this setting, restart Docker Desktop, and redeploy.

Serverless Framework — deploys the Lambda for you

Serverless Framework packages your code and deploys it to AWS with one command. Install it globally:

npm install -g serverless

Verify with npx serverless --version (or sls --version if installed globally).

LLM Provider — powers Claudia's brain (choose one)

Option A: Anthropic API (simplest)

  1. Go to console.anthropic.com/settings/keys
  2. Create a new API key (starts with sk-ant-...)

Option B: Amazon Bedrock (uses your AWS account, no API key needed)

  1. Open Amazon Bedrock consoleModel access
  2. Enable access to Claude models (e.g. Claude Sonnet)
  3. The setup script handles the rest — IAM permissions are included automatically

Option C: Google Gemini

  1. Go to aistudio.google.com/apikey
  2. Create an API key
  3. Gemini 3.x models supported (Flash, Pro, Flash Lite)
  4. All tools supported (bash, web_search, web_fetch)

Option D: OpenAI

  1. Go to platform.openai.com/api-keys
  2. Create a new API key
  3. GPT-4o, GPT-5, o3, o4-mini models supported
  4. All tools supported (bash, web_search via built-in, web_fetch)

🔧 Step 1: Clone

git clone <your-repo-url> claudia
cd claudia

The setup script will install dependencies for you. Or run npm install / yarn install manually.


💬 Step 2: Create a Slack App

This gives Claudia a presence in your Slack workspace — an identity, permissions, and a way to receive messages.

⚡ Quick way: Use the included manifest to create the app with all settings pre-configured:

  1. Go to api.slack.com/appsCreate New AppFrom a manifest
  2. Pick your workspace, then paste the contents of slack-manifest.example.json
  3. The manifest includes placeholder URLs (YOUR-LAMBDA-URL) — that's fine, you'll update them after deploying (see Step 5)
  4. Click Create
  5. Go to Install App in the sidebar → click Install to Your WorkspaceAllow
  6. Skip to step 2f to collect your credentials

⚠️ Scope changes require reinstall: If you update the manifest or add scopes later, you must reinstall the app (Install AppReinstall to Workspace). Reinstalling generates a new bot token — copy it from OAuth & Permissions and update it in Secrets Manager. The old token is revoked immediately.

🔧 Manual way: If you prefer to set things up yourself:

2a. Create the app

  1. Go to api.slack.com/apps → click Create New App → choose From scratch
  2. Name it Claudia (or whatever you like) and pick your workspace

2b. Set permissions (what Claudia is allowed to do)

Go to OAuth & Permissions in the sidebar, scroll to Scopes, and add these Bot Token Scopes:

Scope Why
app_mentions:read So Claudia can see when someone @mentions her
channels:history So she can read thread replies
channels:read So she can look up channel info (names)
chat:write So she can respond
groups:history So she can work in private channels too
im:history So she can work in direct messages
mpim:history So she can work in group direct messages
reactions:write So she can show status with emoji reactions (💭 🔨 ✅)
assistant:write So she can use the Slack AI streaming API (required for AI container / Agents & AI Apps mode)
users:read So she knows who's talking (display names)

⚠️ Important: If you add or change scopes after the app is already installed, you must reinstall the app for the changes to take effect. Go to Install AppReinstall to Workspace. This generates a new bot token — update it in Secrets Manager.

2c. Subscribe to events (what triggers Claudia)

Go to Event Subscriptions in the sidebar:

  1. Toggle Enable Events to On
  2. Under Subscribe to bot events, add:
    • app_mention — triggers when someone types @claudia
    • message.channels — triggers on thread replies
    • app_home_opened — triggers when someone opens the App Home tab
    • assistant_thread_started — triggers when a new AI container (DM) thread starts
    • assistant_thread_context_changed — triggers when the user's viewing context changes in an AI container thread
  3. Don't set the Request URL yet — you'll get it after deploying

2d. Enable App Home (for OAuth account connections)

Go to App Home in the sidebar:

  1. Under Show Tabs, ensure Home Tab is toggled On

2e. Enable interactivity (for the Retry button and connect/disconnect buttons)

Go to Interactivity & Shortcuts in the sidebar:

  1. Toggle to On
  2. Don't set the URL yet — same as above

2f. Install the app

  1. Go to Install App in the sidebar → click Install to Workspace → Authorize
  2. Copy the Bot User OAuth Token (starts with xoxb-) — you'll need this

2g. Collect your credentials

You need 2 things from the Slack app. The setup script auto-detects the Bot User ID from your token.

Credential Where to find it
🔑 Bot Token OAuth & Permissions → Bot User OAuth Token (xoxb-...)
🔐 Signing Secret Basic Information → App Credentials → Signing Secret

⚡ Step 3: Run Setup

The interactive setup script walks you through the rest. It will:

  • Verify your AWS credentials and Node.js version
  • Let you choose your LLM provider (Anthropic API, Amazon Bedrock, Google Gemini, or OpenAI)
  • Fetch available models and let you pick one
  • Ask for your Slack credentials
  • Optionally configure service integrations (GitHub, Google, etc.)
  • Create an AWS Secrets Manager secret (securely stores your keys)
  • Generate your env.yml config file
  • Install dependencies and run tests to verify everything works
./setup.sh
   ____ _                 _ _
  / ___| | __ _ _   _  __| (_) __ _
 | |   | |/ _` | | | |/ _` | |/ _` |
 | |___| | (_| | |_| | (_| | | (_| |
  \____|_|\__,_|\__,_|\__,_|_|\__,_|

   Let's get your AI assistant up and running!

Checking prerequisites...
   ✓ AWS CLI
   ✓ Node.js v22.22.1
   ✓ npm 10.9.4

☁️  Step 1: AWS Profile
? AWS profile name (blank for default): myprofile
   ✓ Authenticated to AWS account 123456789012

⚙️  Step 2: AWS Configuration
? Organization name: mycompany
? AWS region: us-east-1

🧠 Step 3: LLM Provider
? Choose provider: Anthropic API — direct API key, simplest setup
? Anthropic API Key: ****
   ✓ Found 6 models
? Choose model: Claude Sonnet 4.6 — claude-sonnet-4-6

💬 Step 4: Slack App Credentials
? Slack Bot Token: ****
? Slack Signing Secret: ****
   ✓ Bot user: claudia (U01ABC123)

🔗 Step 5: Service Integrations (optional)
? Configure any service integrations now? No
   Skipping service setup. Edit config/services.yml when ready.

🔒 Step 6: Secret Storage
? Secret name: mycompany-claudia-us-east-1
   ✓ Secret 'mycompany-claudia-us-east-1' created.

🎉 Setup complete!

The setup uses an interactive CLI with arrow-key selection for providers and models. It fetches the latest available models dynamically from the Anthropic API or Bedrock. Gemini models use a hardcoded list (Flash, Pro, Flash Lite). OpenAI models use a hardcoded list (GPT-4o, GPT-5, o3, o4-mini).

🔒 Security: Your API keys are stored in AWS Secrets Manager — never in config files, never in git. The Lambda fetches them at runtime.

You can also configure manually if you prefer.

Admin Menu

You can also use npm run setup to re-run the wizard, or npm run admin for the menu. Running ./setup.sh again opens the admin menu:

? What would you like to do?
  🚀 Initial Setup        — First-time installation wizard
  💬 Manage Slack Apps    — Bot config, AI container, manifests (per-stage)
  🔒 Manage Secrets       — Add/remove keys in Secrets Manager (per-stage)
  🔗 Manage Services      — Configure service integrations
  ⚙️  Agent Config         — Provider, model, budget, tools
  🏠 Local Development    — LocalStack, local server, ngrok
  📦 Deploy               — Deploy to dev or prod
  🔍 Health Check         — Validate configuration + connectivity
  📊 Status               — Show current configuration
  ──────────────
  ❌ Exit
Menu item What it does
Initial Setup Re-runs the full first-time wizard (8 guided steps with info boxes, tips, and validation)
Manage Slack Apps Per-stage Slack configuration: auto-detect bot user ID via auth.test, AI container settings, generate manifests, test connectivity, migrate from env.yml
Manage Secrets Add, update, or remove keys in your Secrets Manager secret. Warns if LocalStack is running (stale secrets)
Manage Services Per-service setup wizards (Google Workspace has a guided wizard with gcloud automation, API scope selection, credential management, and health checks)
Agent Config Change model, budget, max turns, and other config/agent.yml settings
Local Development Setup/manage LocalStack, start local server, start with ngrok, re-seed secrets, check status
Deploy Build Docker image, push to ECR, deploy via CloudFormation. Prompts to update manifest after
Health Check Quick local check (instant) or full remote validation per stage (AWS, Slack, API, stack status)
Status Show current configuration summary (includes manifest URL status)

The menu detects whether env.yml exists — first-time users go straight to the setup wizard, returning users get the menu. Use --stage dev or --stage prod to skip stage prompts (e.g. npm run admin -- --stage dev).


🚢 Step 4: Deploy

The easiest way is through the admin menu (npm run adminDeploy). This builds the Docker image, pushes to ECR, and deploys via CloudFormation.

Or deploy manually (requires Docker running):

npm run dep:dev

The first deploy takes 3-5 minutes (builds Docker image with CLI tools). Subsequent deploys with only code changes are faster (~1-2 minutes) thanks to Docker layer caching. When it finishes, you'll see output like:

endpoints:
  claudia: https://abc123xyz.lambda-url.us-east-1.on.aws/

📋 Copy that URL — you need it for the next step.

💡 What just happened? Serverless Framework created a Lambda function, two S3 buckets (one for deployments, one for sessions/locks/storage/prompts), a DynamoDB table (for OAuth tokens), IAM permissions, and a public URL — all defined in serverless.yml. You can tear it all down with npx serverless remove -s dev (or yarn serverless remove -s dev).


🔗 Step 5: Connect Slack

Now point your Slack app at the Lambda. The easiest way is to use the admin menu:

  1. Run ./setup.shSlack Manifest (or the deploy step will prompt you automatically)
  2. Select the Function URL for your deployed stage
  3. Go to api.slack.com/apps → your app → App Manifest → paste the contents of the updated slack-manifest.json

Manual alternative: Copy the Function URL from the deploy output, then:

  • Event Subscriptions → paste as the Request URL (Slack will verify it)
  • Interactivity & Shortcuts → paste the same URL

🎉 Step 6: Try It!

Head to any channel in your Slack workspace and type:

@claudia hello! What can you do?

You should see:

  1. A "thinking..." status appear immediately
  2. A streaming reply from Claudia that builds in real-time
  3. Thinking and tool steps appear as collapsible task cards in the plan block
  4. Usage stats and feedback buttons when she's done

You're all set! 🎊

💡 Next step: Edit config/prompts/tools-prompt.md to tell Claudia about your specific tools, databases, and workflows. This is what makes her truly useful for your team.


💬 How Conversations Work

Claudia uses Slack threads as conversations. Each thread is a separate session with its own memory.

You:      @claudia What's our current error rate?
Claudia:  📊 Looking that up... [queries monitoring tool]
          Error rate is 0.3% over the last 24h, down from 0.5% yesterday.

You:      What about the P99 latency?              ← no @mention needed
Claudia:  P99 latency is 450ms, which is within the SLA target of 500ms.

Bob:      Can you break that down by endpoint?      ← multi-user works!
Claudia:  Here's the breakdown by endpoint...

Key behaviors:

  • 🆕 @mention in any channel → starts a new conversation
  • 💬 Reply in thread → continues the conversation (no @mention needed)
  • 👥 Multiple users can participate — Claudia tracks who said what
  • 🔄 Sessions persist — come back to a thread days later and Claudia remembers
  • Real-time streaming — response text, thinking steps, and tool cards stream in live via Slack's AI streaming API
  • 🤖 AI container (DM mode) — open Claudia directly in Slack (as an AI App) for private 1:1 conversations; sets a thread title and suggested prompts on start
  • 🎯 Live task cards — thinking and tool execution show as streaming task cards with spinner/checkmark status

⚙️ Configuration

Claudia's behavior is fully customizable through simple config files. Sensible defaults are provided — most users only need to edit one file.

Config Files

config/
├── agent.yml                    # 🎛️ Model, budget, turns, bot name (SDK options only)
├── presenter-slack.yml          # 💬 Slack config: AI container + per-stage bot_user_id (gitignored)
├── presenter-slack.example.yml  # 📋 Template for presenter-slack.yml
├── tools.yml                    # 🔧 Per-tool configuration (bash, skill_load, ask_user, web_search, web_fetch, tool_search)
├── services.yml                 # 🔗 Service integrations (PAT, OAuth, bot_token)
├── services/                    # 📖 Per-service tool prompts (e.g., google.md)
├── tools/                       # 📖 Per-tool prompts (e.g., bash.md, skills.md)
├── prompts/
│   ├── system-prompt.md         # 🧠 System prompt (role, tone, formatting)
│   ├── tools-prompt.md          # 🔧 Tool instructions — customize this!
│   └── user-prompt.md           # 💬 User prompt wrapper

config/agent.yml

Controls the agent's capabilities and limits:

bot_name: Claudia                  # Your bot's display name (used in prompts)
provider: anthropic                # LLM provider: 'anthropic' (default), 'bedrock', 'gemini', 'openai'
model: claude-sonnet-4-6           # Bedrock: global.anthropic.claude-sonnet-4-6 | Gemini: gemini-3-flash-preview
                                   # OpenAI: gpt-4o, gpt-5, o3, o4-mini
max_tokens: 16384                  # Max output tokens per API call
max_turns: 10                      # Max tool-use round trips
max_budget_usd: 1.0
stream_update_interval_ms: 3000
effort: medium                     # Reasoning depth: low, medium, high, max

# Tools: list tool names to enable. All client-side except tool_search.
allowed_tools:
    - bash
    - skills
    - ask_user                # Interactive multiple-choice questions via Slack buttons
    - web_search              # Client-side: subagent with web search grounding
    - web_fetch               # Client-side: local fetch + html-to-text + SSRF protection
    - tool_search       # Server-side: deferred tool discovery (loads tools on demand)

# mcp_servers:               # MCP server definitions (merged with .mcp.json)
#     github:
#         command: npx
#         args: ["-y", "@modelcontextprotocol/server-github"]
#         env:
#             GITHUB_TOKEN: "${GITHUB_TOKEN}"
Field Type Default Description
bot_name string Claudia Bot's display name (used in prompts via {{bot_name}})
provider string anthropic LLM provider (anthropic, bedrock, gemini, openai)
model string Model ID (Anthropic, Bedrock, or Gemini format)
max_tokens number 16384 Max output tokens per API call
max_turns number 10 Max tool-use round trips per invocation
max_budget_usd number 1.0 Max cost per invocation
stream_update_interval_ms number 3000 Throttle for Slack message updates (ms)
effort string Reasoning depth: low, medium, high, max
tools list [] Tools to enable (e.g. [bash, skill_load, ask_user, web_search, web_fetch, tool_search])
max_storage_mb number 50 Max /tmp archive size (MB) for persistence between turns
mcp_servers object MCP server definitions (see below)

Note: AI container settings (assistant_description, suggested_prompts, default_thread_title) and per-stage bot_user_id are now in config/presenter-slack.yml. See the setup wizard or npm run admin → Manage Slack Apps to configure.

config/tools.yml

Per-tool configuration and definitions, separate from agent.yml. Provider adapters automatically select built-in types or config-driven function declarations depending on the provider.

bash:
    timeout: 30000
    max_output: 16384
    working_dir: /tmp
    proxy:
        enabled: true
        allowed_hosts: [pypi.org, "*.googleapis.com"]

web_search:
    grounding_model: gemini-3.1-flash-lite-preview   # Cheap model for Gemini grounding

web_fetch:
    timeout: 10000
    max_content_length: 250000
    summarize: false                 # Summarize large pages with a cheap model
    summarize_threshold: 50000       # Only summarize above this char count
    summarize_models:                # Cheap model per provider for summarization
        gemini: gemini-3.1-flash-lite-preview
        anthropic: claude-haiku-4-5-20251001
        openai: gpt-5.4-nano

Per-tool behavioral instructions (prompt field) live in config/tools.yml alongside the tool definition. These are automatically appended to {{tools_prompt}} for enabled tools.

Tool implementation by provider:

Tool Anthropic Bedrock Gemini OpenAI
bash Client — custom executor with security controls Client — same Client — same Client — same
skill_load Client — loads .claude/skills/*/SKILL.md on demand Client — same Client — same Client — same
ask_user Client — Slack buttons/checkboxes Client — same Client — same Client — same
web_search Client — subagent via web_search_20250305 Client — same Client — subagent via googleSearch grounding Server — built-in web_search tool
web_fetch Client — local fetch() + html-to-text + SSRF protection Client — same Client — same Client — same
tool_search Server — API built-in (deferred tool discovery) Server — API built-in Not supported Server — built-in tool_search

Server = LLM API executes the tool. Client = Lambda runs the tool locally.

For detailed tool documentation, see docs/TOOLS.md.

config/prompts/tools-prompt.md — ⭐ the file you'll customize

This is where you define what Claudia can do with her tools. It gets injected into the system prompt via {{tools_prompt}}. Tell her about your databases, APIs, repos — whatever you've connected.

<tools>
You have access to tools that extend your capabilities beyond conversation.
Use them proactively when a task requires looking up information, querying
data, or taking action — don't just describe what you would do.

When using tools:
- Use the right tool for the job. Query databases directly rather than
  asking the user to look something up.
- Combine multiple tool calls when needed to complete a task.
- Summarize results clearly — pull out key information rather than
  pasting raw output.
</tools>

config/prompts/system-prompt.md (rarely needs changing)

Controls Claudia's personality, tone, formatting rules, and safety guidelines. Covers Slack-specific best practices (mrkdwn formatting, thread etiquette, multi-user awareness, prompt injection protection, etc.).

config/prompts/user-prompt.md (rarely needs changing)

Wraps each message with the sender's identity for multi-user thread attribution:

[{{user_name}} ({{user_id}})]: {{message}}

🔧 MCP Servers

Connect Claudia to external tools and services using MCP servers. Define them in agent.yml and/or a .mcp.json file at the project root — both are merged, with agent.yml taking precedence.

Environment variables in MCP configs (${VAR_NAME}) are resolved from Lambda environment variables at runtime.

🎯 Skills (On-Demand Instructions)

Claudia supports a skill system for progressive instruction disclosure. Instead of putting all specialized instructions in the system prompt, skills are listed as a compact catalog (~name + description) and loaded on demand when the model needs them.

How it works:

  1. Skills are defined as SKILL.md files in .claude/skills/{name}/ with YAML frontmatter (name, description)
  2. At config load, loadSkills() scans for skills and buildSkillCatalog() formats a compact catalog
  3. The catalog is appended to {{tools_prompt}} in the system prompt
  4. When a user request matches a skill, the model calls the skills tool to load the full instructions
  5. The skill body is returned with $ARGUMENTS and ${CLAUDE_SKILL_DIR} substituted

Skill file format:

---
name: my-skill
description: Short description shown in the catalog
---

Full instructions loaded on demand...
Run: $ARGUMENTS

Adding skills: Create a directory under .claude/skills/ with a SKILL.md file. Skills without a description in frontmatter are skipped. The skill_load tool must be enabled in config/agent.yml (tools: [bash, skill_load]ask_user, web_search, web_fetch, and tool_search are also available).

🔗 Service Integrations (Per-User Connections)

Let users connect their own accounts so Claudia can authenticate on their behalf. Users manage connections from the App Home tab. Services are defined in config/services.yml (separate from agent.yml).

Three auth patterns:

Type How it works Example
pat User pastes a Personal Access Token via Slack modal GitHub, Linear, Notion
oauth Browser redirect flow, tokens auto-refresh Google Workspace
bot_token Uses a shared token from Secrets Manager, always connected Slack MCP

Multi-account support: Users can connect multiple instances of the same service with labels (e.g., github#personal, github#work). Each instance gets its own MCP server and env var (GITHUB_PERSONAL_TOKEN, GITHUB_WORK_TOKEN).

Example config:

# config/services.yml
services:
    github:
        type: pat
        instructions: "Create a fine-grained PAT at https://github.com/settings/tokens?type=beta"
        scopes_hint: "Repository access: read/write"
        mcp_server:
            command: ./bin/github-mcp-server
            args: ["--toolsets=repos,issues,pull_requests,code_search"]
            env:
                GITHUB_PERSONAL_ACCESS_TOKEN: "{{token}}"

    google:
        type: oauth
        client_id: "your-google-client-id"
        authorize_url: "https://accounts.google.com/o/oauth2/v2/auth"
        authorize_params:
            access_type: offline
            prompt: consent
        token_url: "https://oauth2.googleapis.com/token"
        scopes: "https://www.googleapis.com/auth/drive ..."
        env_alias: GOOGLE_WORKSPACE_CLI_TOKEN  # Auto-set so gws CLI picks up the token

    slack:
        type: bot_token
        token_source: SLACK_BOT_TOKEN
        mcp_server:
            command: ./bin/slack-mcp-server
            args: ["--transport", "stdio"]
            env:
                SLACK_MCP_XOXB_TOKEN: "{{token}}"

Per-service tool prompts: Create config/services/{name}.md (e.g., config/services/google.md) to tell the agent how to use services that don't have MCP servers. These are automatically appended to {{tools_prompt}} for connected services.

env_alias: Services can define an env_alias field to set an additional env var pointing to the same token. This lets CLI tools pick up the token automatically (e.g., gws reads GOOGLE_WORKSPACE_CLI_TOKEN). When multiple instances are connected, the alias is set from the user's default instance (first connection auto-becomes default; users can change via App Home's "Set as default" button). Important: by default, all service env vars (including aliases) are stripped from tool child processes for security. To allow a tool to access a service's env vars, add the service name to the tool's services list in config/tools.yml:

bash:
    services: [google]  # google's token + env_alias available to bash commands

Setup:

The easiest way is through the admin menu: npm run adminManage Services. Google Workspace has a guided wizard that:

  • Walks through GCP project creation/selection
  • Lets you choose which Google APIs to enable (checkbox with descriptions)
  • Auto-enables APIs via gcloud CLI (with manual fallback)
  • Guides OAuth consent screen configuration
  • Collects and stores credentials (services.yml + Secrets Manager)
  • Includes health check, scope modification, and credential update flows

For other services, define them in config/services.yml and add secrets via the Secrets menu. Users connect via the App Home tab.

Template Variables

All prompt files support {{variable}} placeholders replaced with Slack context at runtime:

System prompt (consistent for the thread):

Variable Description Example
{{bot_name}} Bot's display name (from agent.yml) Claudia
{{channel_name}} Channel name general
{{channel}} Channel ID C01ABC123
{{thread_ts}} Thread timestamp 1234567890.123456
{{team_id}} Slack workspace ID T01ABC123
{{tools_prompt}} Contents of prompts/tools-prompt.md (injected)
{{connected_services}} User's connected services, one per line with structured fields - service=google, label=work, env=$GOOGLE_WORK_TOKEN, default=true
{{viewing_channel}} Channel the user is viewing when using the AI container (DM mode); empty for channel mentions C01ABC123

User prompt (changes with each message):

Variable Description Example
{{message}} The user's message (mentions stripped) What is the weather?
{{user_name}} Slack display name Alice Smith
{{user_id}} Slack user ID U01ABC123

🏗️ Architecture

flowchart TD
    Slack["💬 Slack Event"] --> URL["⚡ Lambda Function URL"]
    URL --> Filter{"🔍 Filter"}
    Filter -- "retry / bot" --> Reject["✋ Reject (200)"]
    Filter -- "valid" --> Ack["✅ Verify + Ack (200)"]
    Ack --> Async["🔀 Self-invoke async"]
    Async --> Lock["🔒 Acquire S3 lock"]
    Lock --> Post["💭 Post 'Thinking...'"]
    Post --> Load["📂 Load session from S3"]
    Load --> Render["📝 Render prompt templates"]
    Render --> Build["🔧 Build API params"]
    Build --> Agent["🤖 Stream from params"]
    Agent --> Stream["📡 Stream events to presenter"]
    Stream --> Save["💾 Save request + response to S3"]
    Save --> Unlock["🔓 Release lock"]
Loading

Each Slack thread maps to one session file stored as JSONL in S3. Each turn produces two lines: the full API request params and the full response (content, stop reason, usage). On the next turn, messages are reconstructed from the last request/response pair and the new user message is appended. Additionally, /tmp contents are archived to S3 as a gzipped tarball after each successful invocation and restored at the start of the next turn, so files created by tool execution persist across the conversation.

Architecture Overview

graph TB
    subgraph Slack["Slack"]
        User["👤 User"]
        SlackAPI["Slack API"]
    end

    subgraph AWS["AWS"]
        Lambda["Lambda<br/>(Docker container)"]
        subgraph S3["S3 Bucket"]
            Sessions["sessions/*.jsonl"]
            Locks["locks/*"]
            Storage["storage/*.tar.gz"]
            Prompts["prompts/*.txt"]
        end
        DDB["DynamoDB<br/>(TokenTable)"]
        Secrets["Secrets Manager"]
    end

    subgraph LLM["LLM Provider"]
        Anthropic["Anthropic API"]
        Bedrock["Amazon Bedrock"]
    end

    User -->|"@mention / DM"| SlackAPI
    SlackAPI -->|"Event (Function URL)"| Lambda
    Lambda -->|"Ack 200 < 3s"| SlackAPI
    Lambda -->|"Self-invoke async"| Lambda
    Lambda <-->|"Lock, Session, Storage"| S3
    Lambda <-->|"User tokens, OAuth state"| DDB
    Lambda -->|"API keys"| Secrets
    Lambda <-->|"Stream"| Anthropic
    Lambda <-->|"Stream"| Bedrock
    Lambda -->|"startStream / appendStream"| SlackAPI
Loading

DynamoDB Schema (Single-Table)

One table (TokenTable) stores all records using a composite key (pk + sk):

pk sk Record type TTL Key fields
USER#{team}#{user} SVC#{service}#{label} Service token 1 year access_token, token_type, refresh_token, scopes, expires_at, connected_at, updated_at, version, is_default
STATE#{uuid} STATE OAuth CSRF state 10 min team_id, user_id, service, label
  • Token records — One per connected service instance per user. version enables optimistic locking for token refresh. is_default designates the active default when multiple instances of the same service exist.
  • State records — Ephemeral, consumed on first use (atomic delete + return). Prevents CSRF in OAuth flows.

S3 Object Layout

All objects in the service bucket ({org}-claudia-{region}-{stage}):

Prefix Format Lifecycle Purpose
sessions/{team}/{channel}/{thread}.jsonl JSONL (request + response pairs) 30 days Conversation history
locks/{team}/{channel}/{thread} JSON (ownerId, timestamp) 1 day Thread locking
storage/{team}/{channel}/{thread}.tar.gz Gzipped tarball 30 days Persisted /tmp between turns
prompts/{team}/{channel}/{thread}/{ts}.txt Plain text 30 days Human-readable prompt snapshots

Request Lifecycle

sequenceDiagram
    actor User
    participant Slack
    participant Lambda
    participant S3
    participant LLM as LLM Provider

    User->>Slack: @claudia help me with...
    Slack->>Lambda: Event (Function URL)
    Lambda->>Slack: Ack (200) within 3s
    Lambda->>Lambda: Self-invoke async

    Lambda->>S3: Acquire thread lock
    Lambda->>S3: Restore /tmp storage archive
    Lambda->>S3: Load session (reconstruct messages)
    Lambda->>LLM: Build params + stream
    LLM-->>Lambda: thinking_delta, text_delta events
    Lambda-->>Slack: Update single message (plan + markdown blocks)
    LLM->>Lambda: message_complete
    Lambda->>Slack: Finalize message with usage stats ✅
    Lambda->>S3: Save request params + response
    Lambda->>S3: Save /tmp storage archive
    Lambda->>S3: Release lock
Loading

Thread Routing

flowchart TD
    Event["📨 Slack Event"] --> IsBot{"🤖 Bot message?"}
    IsBot -- Yes --> Drop["🚫 Drop (prevent loops)"]
    IsBot -- No --> IsMention{"📣 @mention?"}
    IsMention -- Yes --> New["🆕 New agent session"]
    IsMention -- No --> InThread{"🧵 In existing thread?"}
    InThread -- Yes --> Resume["🔄 Resume session from S3"]
    InThread -- No --> Ignore["💤 Ignore"]
    New --> Process["▶️ Process with user context"]
    Resume --> Process
Loading

🛡️ Reliability & Race Condition Protections

Claudia is built to be robust in a busy Slack workspace:

Protection Problem How it works
Slack retry rejection Lambda takes too long → Slack retries up to 3 times → one message triggers 3 parallel runs Checks x-slack-retry-num header and immediately returns 200 to discard retries
Bot message filter Bot's own responses are Slack events too → infinite loop Skips events with bot_id, subtype bot_message, or message_changed
Event deduplication @claudia help fires two events (app_mention + message) → processed twice Uses getBotUserId() (from presenter-slack.yml → env var fallback) to detect and skip the duplicate message event
S3 thread lock Two users post simultaneously → two Lambdas modify the same session Acquires lock in S3 via conditional writes. Second invocation notifies the user with a Retry button

When a message hits a locked thread, the sender gets an ephemeral message (only they can see) with a Retry button — no messages are silently lost.

🔒 Security

Claudia has multiple security layers protecting against credential leakage and unauthorized access:

  • Request authentication — Slack signature verification (HMAC-SHA256, timing-safe) before body parsing
  • Credential isolation — AWS creds and user tokens stripped from child processes; per-tool service scoping
  • Command safety — Blocked patterns, resource ulimits, timeouts, output truncation, secret redaction
  • Egress proxy — In-process HTTP forward proxy restricts outbound requests to allowlisted hostnames (bash.proxy in config/tools.yml)
  • Infrastructure — Lambda container isolation, S3 encryption, DynamoDB encryption + PITR

See SECURITY.md for comprehensive documentation.

📚 Further Reading


📁 Project Structure

claudia/
├── config/
│   ├── agent.yml            # 🎛️ Model, MCP servers, budget (SDK options)
│   ├── tools.yml            # 🔧 Per-tool configuration (bash, skill_load, ask_user, web_search, web_fetch, tool_search)
│   ├── services.yml         # 🔗 Service integrations (PAT, OAuth, bot_token)
│   ├── services/            # 📖 Per-service tool prompts (e.g., google.md)
│   ├── tools/               # 📖 Per-tool prompts (e.g., bash.md, skills.md)
│   └── prompts/
│       ├── system-prompt.md # 🧠 System prompt (role, tone, formatting)
│       ├── tools-prompt.md  # 🔧 Tool instructions (customize this!)
│       └── user-prompt.md   # 💬 User prompt wrapper
├── src/
│   ├── handler.js           # ⚡ Lambda entry point — verify, ack, filter, route
│   ├── agent.js             # 🤖 Agentic loop with tool execution (UI-agnostic)
│   ├── presenters/          # 🎨 Presenter abstraction (base, slack, factory)
│   ├── tools/               # 🔧 Tool executors (bash, skills, web-search, web-fetch, ask-user)
│   ├── aws.js               # ☁️ AWS SDK config (LocalStack endpoint in local mode)
│   ├── llm-providers/       # 🧠 LLM provider abstraction (Anthropic, Gemini, OpenAI)
│   ├── appHome.js           # 🏠 App Home tab view builder
│   ├── config.js            # 📋 Config loading & template rendering
│   ├── lock.js              # 🔒 S3-based thread lock (with ownership)
│   ├── oauth.js             # 🔐 OAuth flow (callback, token exchange, refresh)
│   ├── secrets.js           # 🔑 AWS Secrets Manager client
│   ├── sessions.js          # 💾 Session storage (JSONL) + prompt snapshots (plain text) in S3
│   ├── storage.js           # 📦 /tmp persistence between turns (tar.gz to S3)
│   ├── slack.js             # 💬 Slack API helpers
│   ├── tokens.js            # 🎫 DynamoDB token CRUD & OAuth state
│   └── __tests__/           # 🧪 Jest tests
├── scripts/
│   ├── local.js             # 🖥️ Local dev server (wraps handler in HTTP server)
│   ├── local-docker.js      # 🐳 Run local dev server inside production Docker image
│   ├── docker-ngrok.js      # 🌐 Docker local dev + ngrok tunnel (one command)
│   ├── bootstrap-local.js   # 🏗️ Seeds LocalStack with S3/DynamoDB/Secrets
│   └── logs.js              # 📋 CloudWatch log viewer
├── Dockerfile               # 🐳 Multi-stage Docker build (CLI tools + runtime)
├── docker-compose.yml       # 🐳 LocalStack for local development
├── .dockerignore            #    Excludes .git, node_modules, env.yml from build
├── layers/                  # 📦 CLI install scripts (used by Dockerfile)
│   ├── install-gws.sh       #    Downloads gws binary for linux-x64
│   └── install-awscli.sh    #    Downloads AWS CLI v2 for linux-x64
├── .claude/
│   ├── settings.json        # 🔌 Claude Code plugin config (tracked)
│   └── skills/
│       └── claude-session-jsonl/  # 📊 Skill for analyzing session JSONL files
│           ├── SKILL.md           #    Skill instructions & script reference
│           ├── format.md          #    JSONL format documentation
│           └── scripts/           #    summary, conversation, tools, tokens, search, export
├── .mcp.json                # 🧩 MCP server definitions (optional)
├── serverless.yml           # ☁️ Lambda + Function URL + S3 + IAM
├── env.yml                  # 🔧 Stage-based deployment config
├── slack-manifest.example.json  # 📋 Slack app manifest template (tracked)
├── slack-manifest.json      # 📋 Generated manifest with real URLs (gitignored)
├── admin.js                 # 🚀 Setup wizard (npm run setup) & admin menu (npm run admin)
├── env.js                   # 🔖 Git commit hash for stack tags + deploy guard
├── setup.sh                 # 🚀 Thin wrapper — checks Node, runs admin.js
└── package.json

🧪 Testing

npm test     # or: yarn test

📋 npm Scripts Reference

Script Command Description
npm test LOCAL=1 jest Run all tests
Deploy
npm run dep:dev serverless deploy -s dev Deploy to dev (Docker build + ECR push + CloudFormation)
npm run dep:prod serverless deploy -s prod Deploy to prod
Logs
npm run logs:dev node scripts/logs.js dev --follow Tail Lambda logs (dev, streaming)
npm run logs:prod node scripts/logs.js prod --follow Tail Lambda logs (prod, streaming)
npm run logs:dev:recent node scripts/logs.js dev Show recent Lambda logs (dev, no follow)
npm run logs:prod:recent node scripts/logs.js prod Show recent Lambda logs (prod, no follow)
Local Dev
npm run local:setup docker compose up -d && bootstrap One-time: start LocalStack + seed resources
npm run local node --watch scripts/local.js HTTP server on :3000 with hot reload
npm run local:ngrok node scripts/ngrok.js Local server + ngrok tunnel for real Slack events
npm run local:docker node scripts/local-docker.js Run inside production Docker container
npm run local:docker:ngrok node scripts/docker-ngrok.js Docker + ngrok tunnel
npm run local:proxy local.js with HTTP_PROXY Local server with HTTP proxy for traffic inspection
npm run local:proxy:ngrok ngrok.js with HTTP_PROXY Local + ngrok + proxy
npm run proxy mitmdump --listen-port 8080 Start mitmproxy for inspecting LLM API traffic
Auth & Setup
npm run aws:login aws sso login Authenticate with AWS SSO
npm run setup node admin.js --setup Run initial setup wizard
npm run admin node admin.js Open admin menu (secrets, services, deploy, etc.)

🖥️ Local Development

Run Claudia locally with hot reload and LocalStack for AWS services. Changes restart instantly — no Docker rebuild or deploy needed.

Prerequisites

  • Docker — for LocalStack (S3, DynamoDB, Secrets Manager emulation)
  • AWS credentials — the bootstrap script fetches your real secrets from AWS to seed LocalStack

Setup (one-time)

npm run local:setup

This starts LocalStack via docker compose, then creates the S3 bucket, DynamoDB table, and Secrets Manager secret (copied from your real AWS dev environment).

Run

npm run local                   # HTTP server on :3000 with hot reload (node --watch)
npm run local:ngrok             # Same + ngrok tunnel for real Slack events

Set the ngrok URL as your Slack app's Request URL (Event Subscriptions + Interactivity) to receive real events locally.

Test with curl

# URL verification (quick sanity check)
curl -XPOST http://localhost:3000 \
  -H 'Content-Type: application/json' \
  -d '{"type":"url_verification","challenge":"test123"}'

# Simulate a Slack event (signature verification is skipped in local mode)
curl -XPOST http://localhost:3000 \
  -H 'Content-Type: application/json' \
  -d '{"type":"event_callback","team_id":"T123","event":{"type":"app_mention","user":"U456","channel":"C789","text":"hello","ts":"1234567890.123456"}}'

How it works

  • LOCAL=1 env var makes all AWS SDK clients point to LocalStack (localhost:4566)
  • Lambda self-invokes are handled in-process via setImmediate instead of calling the Lambda API
  • Secrets, S3 sessions, DynamoDB tokens all work against LocalStack
  • Slack API calls (posting messages, reactions) still hit real Slack — you need a valid bot token in your secret

Docker-based local dev (production-like)

Run inside the production Docker container for production-like behavior (same PATH, filesystem layout, and CLI tools as Lambda):

npm run local:docker            # Build + run in Docker, HTTP on :3000
npm run local:docker:ngrok      # Same + ngrok tunnel for real Slack events

The container reaches LocalStack via host.docker.internal. Requires npm run local:setup first.

Traffic inspection (optional)

Inspect LLM API requests/responses with mitmproxy:

npm run proxy                   # Start mitmproxy on :8080
npm run local:proxy             # Local server routing traffic through the proxy
npm run local:proxy:ngrok       # Same + ngrok tunnel

📖 Manual Configuration

Click to expand manual setup instructions

Store Secrets in AWS Secrets Manager

Create a secret named <org>-claudia-<region>-<stage> (e.g. mycompany-claudia-us-east-1-dev):

For Anthropic API:

{
    "SLACK_BOT_TOKEN": "xoxb-your-bot-token",
    "SLACK_SIGNING_SECRET": "your-signing-secret",
    "ANTHROPIC_API_KEY": "sk-ant-your-api-key"
}

For Amazon Bedrock (IAM auth — recommended):

{
    "SLACK_BOT_TOKEN": "xoxb-your-bot-token",
    "SLACK_SIGNING_SECRET": "your-signing-secret"
}

For Amazon Bedrock (API key auth):

{
    "SLACK_BOT_TOKEN": "xoxb-your-bot-token",
    "SLACK_SIGNING_SECRET": "your-signing-secret",
    "BEDROCK_API_KEY": "your-bedrock-api-key"
}

For Google Gemini:

{
    "SLACK_BOT_TOKEN": "xoxb-your-bot-token",
    "SLACK_SIGNING_SECRET": "your-signing-secret",
    "GEMINI_API_KEY": "your-gemini-api-key"
}

For OpenAI:

{
    "SLACK_BOT_TOKEN": "xoxb-your-bot-token",
    "SLACK_SIGNING_SECRET": "your-signing-secret",
    "OPENAI_API_KEY": "sk-your-openai-api-key"
}

With service secrets (OAuth client secrets, bot tokens) — added as keys to the same secret:

{
    "SLACK_BOT_TOKEN": "xoxb-your-bot-token",
    "SLACK_SIGNING_SECRET": "your-signing-secret",
    "ANTHROPIC_API_KEY": "sk-ant-your-api-key",
    "GOOGLE_CLIENT_SECRET": "your-google-oauth-client-secret"
}

Configure env.yml

Copy the template and fill in your values:

cp env.example.yml env.yml

See env.example.yml for all available options with descriptions. The secret name is derived automatically as <ORG>-claudia-<region>-<stage> (e.g. myorg-claudia-us-east-1-dev). env.yml contains only AWS infrastructure settings (org, region, buckets, profile). SLS_BUCKET is the deployment bucket (auto-created by the serverless-deployment-bucket plugin). SLS_CLAUDIA_BUCKET is the service bucket for sessions, locks, storage archives, and prompt snapshots. Both are defined per stage in env.yml. Bot user IDs are now stored in config/presenter-slack.yml (managed via npm run admin → Manage Slack Apps).

For Bedrock, set provider: bedrock and update the model in config/agent.yml to use a Bedrock model ID with a routing prefix (e.g. global.anthropic.claude-sonnet-4-6). See Bedrock setup details for prefix options.

For Gemini, set provider: gemini and model: gemini-3-flash-preview (or gemini-3.1-pro-preview, gemini-3.1-flash-lite-preview). Store your API key as GEMINI_API_KEY in Secrets Manager. Note: compaction is not yet supported with Gemini. Gemini uses implicit caching (90% discount, automatic).

For OpenAI, set provider: openai and model: gpt-4o (or o3, o4-mini, gpt-5). Store your API key as OPENAI_API_KEY in Secrets Manager. Web search uses OpenAI's built-in web_search tool (server-side). tool_search uses the built-in tool_search tool (server-side).

🔶 Amazon Bedrock setup details

Using Amazon Bedrock

Claudia can use Claude via Amazon Bedrock instead of the Anthropic API. This keeps all API traffic within your AWS account.

IAM authentication (recommended): The Lambda's IAM role includes Bedrock permissions automatically. No API keys needed — just enable Claude model access in the Bedrock console.

Bedrock API key authentication: If you prefer, store a Bedrock API key as BEDROCK_API_KEY in Secrets Manager.

Setup:

  1. Run ./setup.sh and choose Amazon Bedrock — the setup script fetches available models from Bedrock and lets you pick one, then asks about routing (global vs regional)
  2. Or manually: set provider: bedrock and update the model in config/agent.yml

Model IDs for Bedrock use a {prefix}.{base_model_id} format. The prefix controls request routing:

Prefix Routing Pricing Use case
global. Dynamic routing across regions Standard Recommended — max availability
us. US regions only +10% premium Data must stay in the US
eu. EU regions only +10% premium GDPR / EU data residency
jp. Japan only +10% premium Japan data residency
apac. Asia-Pacific only +10% premium APAC data residency

Available models:

Model Base model ID Global US EU JP APAC
Claude Opus 4.6 anthropic.claude-opus-4-6-v1 Yes Yes Yes Yes Yes
Claude Sonnet 4.6 anthropic.claude-sonnet-4-6 Yes Yes Yes Yes No
Claude Haiku 4.5 anthropic.claude-haiku-4-5-20251001-v1:0 Yes Yes Yes No No

Examples:

  • global.anthropic.claude-sonnet-4-6 — global routing (recommended)
  • us.anthropic.claude-sonnet-4-6 — US regional
  • eu.anthropic.claude-opus-4-6-v1 — EU regional

The setup script fetches available models dynamically and lets you choose the routing mode. See Anthropic's Bedrock documentation for the full model availability matrix.


📄 License

MIT

About

Slack bot powered by Claude Agent SDK. Mention the bot to start a conversation — replies in the thread are handled automatically.

Resources

License

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages