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.
- ✨ What Can Claudia Do?
- 🎯 Why Claudia?
- 🚀 Quick Start
- 💬 How Conversations Work
- ⚙️ Configuration
- 🏗️ Architecture
- 📁 Project Structure
- 🧪 Testing
- 🖥️ Local Development
- 📖 Manual Configuration
- 📄 License
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" |
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.
| 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 |
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.
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 22AWS 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.
- Install from aws.amazon.com/cli
- Run
aws configureand enter your Access Key ID, Secret Access Key, and region - 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 withimage 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 serverlessVerify with npx serverless --version (or sls --version if installed globally).
LLM Provider — powers Claudia's brain (choose one)
Option A: Anthropic API (simplest)
- Go to console.anthropic.com/settings/keys
- Create a new API key (starts with
sk-ant-...)
Option B: Amazon Bedrock (uses your AWS account, no API key needed)
- Open Amazon Bedrock console → Model access
- Enable access to Claude models (e.g. Claude Sonnet)
- The setup script handles the rest — IAM permissions are included automatically
Option C: Google Gemini
- Go to aistudio.google.com/apikey
- Create an API key
- Gemini 3.x models supported (Flash, Pro, Flash Lite)
- All tools supported (bash, web_search, web_fetch)
Option D: OpenAI
- Go to platform.openai.com/api-keys
- Create a new API key
- GPT-4o, GPT-5, o3, o4-mini models supported
- All tools supported (bash, web_search via built-in, web_fetch)
git clone <your-repo-url> claudia
cd claudiaThe setup script will install dependencies for you. Or run
npm install/yarn installmanually.
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:
- Go to api.slack.com/apps → Create New App → From a manifest
- Pick your workspace, then paste the contents of
slack-manifest.example.json - The manifest includes placeholder URLs (
YOUR-LAMBDA-URL) — that's fine, you'll update them after deploying (see Step 5) - Click Create
- Go to Install App in the sidebar → click Install to Your Workspace → Allow
- 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 App → Reinstall 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
- Go to api.slack.com/apps → click Create New App → choose From scratch
- 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 App → Reinstall 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:
- Toggle Enable Events to On
- Under Subscribe to bot events, add:
app_mention— triggers when someone types@claudiamessage.channels— triggers on thread repliesapp_home_opened— triggers when someone opens the App Home tabassistant_thread_started— triggers when a new AI container (DM) thread startsassistant_thread_context_changed— triggers when the user's viewing context changes in an AI container thread
- 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:
- 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:
- Toggle to On
- Don't set the URL yet — same as above
2f. Install the app
- Go to Install App in the sidebar → click Install to Workspace → Authorize
- 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 |
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.ymlconfig 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.
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).
The easiest way is through the admin menu (npm run admin → Deploy). This builds the Docker image, pushes to ECR, and deploys via CloudFormation.
Or deploy manually (requires Docker running):
npm run dep:devThe 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 withnpx serverless remove -s dev(oryarn serverless remove -s dev).
Now point your Slack app at the Lambda. The easiest way is to use the admin menu:
- Run
./setup.sh→ Slack Manifest (or the deploy step will prompt you automatically) - Select the Function URL for your deployed stage
- 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
Head to any channel in your Slack workspace and type:
@claudia hello! What can you do?
You should see:
- A "thinking..." status appear immediately
- A streaming reply from Claudia that builds in real-time
- Thinking and tool steps appear as collapsible task cards in the plan block
- Usage stats and feedback buttons when she's done
You're all set! 🎊
💡 Next step: Edit
config/prompts/tools-prompt.mdto tell Claudia about your specific tools, databases, and workflows. This is what makes her truly useful for your team.
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
Claudia's behavior is fully customizable through simple config files. Sensible defaults are provided — most users only need to edit one file.
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
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-stagebot_user_idare now inconfig/presenter-slack.yml. See the setup wizard ornpm run admin→ Manage Slack Apps to configure.
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-nanoPer-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.
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>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.).
Wraps each message with the sender's identity for multi-user thread attribution:
[{{user_name}} ({{user_id}})]: {{message}}
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.
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:
- Skills are defined as
SKILL.mdfiles in.claude/skills/{name}/with YAML frontmatter (name,description) - At config load,
loadSkills()scans for skills andbuildSkillCatalog()formats a compact catalog - The catalog is appended to
{{tools_prompt}}in the system prompt - When a user request matches a skill, the model calls the
skillstool to load the full instructions - The skill body is returned with
$ARGUMENTSand${CLAUDE_SKILL_DIR}substituted
Skill file format:
---
name: my-skill
description: Short description shown in the catalog
---
Full instructions loaded on demand...
Run: $ARGUMENTSAdding 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).
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 commandsSetup:
The easiest way is through the admin menu: npm run admin → Manage 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
gcloudCLI (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.
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 |
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"]
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.
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
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.
versionenables optimistic locking for token refresh.is_defaultdesignates 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.
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 |
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
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
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.
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.proxyinconfig/tools.yml) - Infrastructure — Lambda container isolation, S3 encryption, DynamoDB encryption + PITR
See SECURITY.md for comprehensive documentation.
docs/TOOLS.md— How the tool system works, available tools, adding new toolsdocs/PROMPT-CACHING.md— Cache breakpoint strategy, system prompt split, cost optimization
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
npm test # or: yarn test| 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.) |
Run Claudia locally with hot reload and LocalStack for AWS services. Changes restart instantly — no Docker rebuild or deploy needed.
- Docker — for LocalStack (S3, DynamoDB, Secrets Manager emulation)
- AWS credentials — the bootstrap script fetches your real secrets from AWS to seed LocalStack
npm run local:setupThis starts LocalStack via docker compose, then creates the S3 bucket, DynamoDB table, and Secrets Manager secret (copied from your real AWS dev environment).
npm run local # HTTP server on :3000 with hot reload (node --watch)
npm run local:ngrok # Same + ngrok tunnel for real Slack eventsSet the ngrok URL as your Slack app's Request URL (Event Subscriptions + Interactivity) to receive real events locally.
# 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"}}'LOCAL=1env var makes all AWS SDK clients point to LocalStack (localhost:4566)- Lambda self-invokes are handled in-process via
setImmediateinstead 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
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 eventsThe container reaches LocalStack via host.docker.internal. Requires npm run local:setup first.
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 tunnelClick to expand manual setup instructions
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"
}Copy the template and fill in your values:
cp env.example.yml env.ymlSee 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
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:
- Run
./setup.shand choose Amazon Bedrock — the setup script fetches available models from Bedrock and lets you pick one, then asks about routing (global vs regional) - Or manually: set
provider: bedrockand update the model inconfig/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 regionaleu.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.
MIT