Drop-in Slack-fronted Claude Code agent. Add a skills/ directory, run `jean init`, deploy to Fly.io.
Project description
Jean
A drop-in Slack-fronted Claude Code agent for your repo. Add a skills/ directory, run jean init, deploy to Fly.io — your team gets a Slack bot that runs Claude Code in your codebase, with conversation history in a web UI.
What it does
- Slack bot. DM it or
@mentionit in a channel. Each Slack thread is one persistent Claude session. - Skills. Drops the host repo's
skills/directory into Claude's project skill discovery — no per-skill registration. A bundledskill-builderskill lets non-tech users create + edit skills by DM'ing the bot. - Owner-gated rollout. Private mode + per-channel approval (the owner gets a DM with Approve/Deny buttons when jean is mentioned in a new channel) + per-thread
cc <@owner>when non-owners invoke jean for the first time. - Two-way attachments. Slack file uploads stage to disk so Claude can
Readthem; skills can callsend_file_to_userto upload artifacts (PDFs, images, charts, CSVs) back into the thread. - Thread-aware. Jean fetches prior human messages in the thread as context. When the SDK session is lost (deploy SIGKILL, volume migration, etc.), jean rebuilds context from the full Slack thread so conversations stay coherent.
- Live-edited replies + progress hourglass. Throttled
chat.updateso Claude's output streams without hitting Slack rate limits; the ⏳ ↔ ⌛ alternation on the user's message indicates the bot is alive even before the first text lands. - Web UI. Sign in with Slack. Users see their own conversations; admins (managed by the owner) see all.
/admin/skills,/admin/channels,/admin/usersfor governance. - Self-diagnosing.
jean doctor --setup-guidewalks you through every secret with the Slack manifest inlined;jean doctor --deepruns live API checks. Misconfigured installs render an owner-only fix-list page instead of the normal UI.
Quick start
You run jean inside your own repo — the one where your skills/ directory
lives. Jean stays in its own private repo and you invoke it via uvx
(npx-equivalent for Python, ships with uv).
Prerequisites:
- uv installed (
brew install uvor the upstream installer) - Your GitHub SSH key configured (jean lives in a private repo;
uvxclones viagit+ssh) - A
skills/<name>/SKILL.mdsomewhere in the repo where you'll run jean (or just create one as you go — the bundledskill-builderskill can help)
Setup, in your host repo:
# 1. Set an alias so you don't have to type the long URL every time
alias jean='uvx --from git+ssh://git@github.com/shinkansenfinance/jean.git jean'
# 2. Scaffold jean.toml + fly.toml + Dockerfile etc. into the current directory
jean init # interactive: Fly app name, owner, bot display name
# 3. Walk through every secret (Anthropic + 4 Slack values) with browser-open + clipboard
jean configure-secrets
# 4. Verify everything is wired up
jean doctor --deep # workspace identity, owner, scopes
# 5. Provision Fly app + volume + secrets, then deploy
jean deploy
Drop the alias into your shell's rc file (~/.zshrc / ~/.bashrc) and the
five commands become permanent.
What jean init writes
jean.toml— root-level config; commit it (no secrets). See Configuration below.fly.toml— Fly machine + volume config (app name, region, mounts, env overrides for/data/...paths).Dockerfile— Python 3.12-slim + Node.js + poppler-utils + LibreOffice + gosu; runs as a non-rootjeanuser..dockerignorejean_system_prompt.md— the host's optional system prompt.
Dockerfile + private repo. The generated
Dockerfileinstalls jean from./.jean-vendor/jean/, a directory thatjean deploypopulates automatically by callingjean vendorright beforefly deploy. The Docker build itself needs zero git / SSH / token access — everything's in the build context. Add/.jean-vendor/to your.gitignore; the wheel-equivalent gets regenerated on every deploy. (For local Docker builds, runjean vendorby hand once, thendocker buildas usual.)
Setting up the Slack app
Skip manual scope-clicking — let jean print the manifest:
jean print-manifest | pbcopy # macOS; use xclip on Linux
Then at https://api.slack.com/apps → Create New App → From a manifest, paste, click Create. The manifest already includes:
- Bot scopes:
chat:write,app_mentions:read,channels:history,channels:read,groups:history,groups:read,im:*,mpim:history,reactions:write,files:read,files:write,users:read,users:read.email - User scopes (SIWS):
openid,email,profile - Redirect URLs (localhost + your Fly app)
- Socket Mode enabled
- Event subscriptions:
app_mention,message.im - Messages Tab enabled (so users can DM the bot from the App Home)
After creation:
- Install App → copy the Bot User OAuth Token (
xoxb-…) - Basic Information → App-Level Tokens → Generate one with
connections:writescope; copy thexapp-…token - Basic Information → App Credentials → copy Client ID and Client Secret
jean configure-secrets walks you through each of these, opens the right pages, hides input, and live-validates the bot token via auth.test.
Deploying to Fly
jean deploy # one command
This wraps flyctl to:
- Check
flyctl auth whoami - Read
fly.toml(app name, region, volume name) - Read encrypted credentials and push them as Fly secrets (staged for the deploy)
- Create the app if missing
- Create the persistent volume (
jean_data, 3 GB default) if missing fly deploy
Flags: --yes, --skip-secrets, --skip-build, --volume-size, --org. Idempotent — rerun safely.
After deploy, open https://<your-app>.fly.dev/, sign in with Slack, and DM the bot.
Configuration
See jean/templates/jean.toml.tmpl for the canonical commented schema. Highlights:
| Section | Field | Default | What it does |
|---|---|---|---|
[jean] |
owner |
git email | @handle, email, or U-id. Auto-promoted to owner role on first login. |
[jean] |
bot_name |
"Jean" |
Display name in Slack (also in the manifest). |
[jean] |
private |
false |
Only the owner can DM the bot. Everyone else gets a polite refusal. |
[jean] |
channel_approval |
true |
First mention in a new channel triggers owner DM with Approve/Deny buttons. |
[claude] |
model |
"claude-opus-4-8" |
All Opus 4.5–4.8 are $5/$25 per M tokens. Sonnet 4.6 ($3/$15) is cheaper. |
[claude] |
thinking |
"adaptive" |
Claude decides per-turn. Set "off" to disable extended thinking. |
[claude] |
effort |
"medium" |
low/medium/high/xhigh/max — guides how aggressively adaptive triggers. |
[claude] |
permission_mode |
"auto" |
auto lets Claude decide; bypassPermissions skips all checks. |
[claude] |
skills_only |
true |
Refuses anything that doesn't map to a skill (keeps token spend in scope). |
[claude] |
user_skill_authors |
"owner" |
owner / admin / any — who can create/edit skills via DM. |
[claude] |
idle_timeout_sec |
300 |
Close SDK session after 5 min idle; resume on next message. |
[slack] |
edit_throttle_sec |
3.0 |
Min seconds between chat.update calls during streaming. |
[slack] |
show_tool_calls |
"on_failure" |
never / on_failure / always |
[attachments] |
max_size_mb |
25 |
Per-attachment cap. Bigger files get a :warning: reply. |
Local dev paths (./.jean/jean.db, ./.jean/uploads) are overridden on Fly to /data/... via fly.toml's [env] block — one jean.toml works in both environments.
Secrets
Five secrets live in an encrypted local store at ~/.config/jean/<project-hash>/credentials.enc (AES-256-GCM, master key in your OS keychain via the keyring package):
ANTHROPIC_API_KEY
SLACK_BOT_TOKEN
SLACK_APP_TOKEN
SLACK_CLIENT_ID
SLACK_CLIENT_SECRET
A sixth, JEAN_SESSION_SECRET (for the SIWS cookie), auto-generates and persists next to the DB.
jean configure-secrets is the canonical way to set them. Env vars override the encrypted file at runtime, so fly secrets set … continues to be the production path — jean deploy reads from the local store and pushes them as Fly secrets in one shot.
Features the bot exposes
Skills created by DM
Users with the configured role (user_skill_authors) can teach the bot new capabilities by DM'ing it. A built-in skill-builder skill drives the conversation:
User: "I want to teach you a daily standup helper."
Bot: "Got it — when I activate this, what should the output look like?"
(few clarifying turns)
Bot: shows the SKILL.md draft → user confirms →
save_skill_filewrites it to/data/skills/standup/SKILL.mdand bumps the metadata in SQLite.
Skills can be multi-file. Editing works through the same surface (list_user_skills → read_skill_file → propose change → save_skill_file). Deletion is web-UI-only at /admin/skills (owner-only). The skill becomes available on the next message — jean automatically closes the session after a save so the new client re-discovers it.
DM-only enforcement is three-layered: visibility gate (skill-builder hidden from non-DM sessions), tool-registration gate (MCP server not attached in channels), tool-implementation gate (each mutating tool re-checks is_dm).
Per-channel approval
When jean is invoked in a channel for the first time (channel_approval = true):
- Bot replies
:hourglass: I need my owner to approve me before I can chat in this channel. I'll respond here once they give me the green light. - The event gets queued in
pending_channel_messages. - Owner receives a DM with Block Kit Approve / Deny buttons.
- On approve: queued messages are replayed through the normal handler — the original asker gets their answer.
- On deny: queued messages are dropped; that channel stays silent.
Revocation at /admin/channels (owner-only) — drops the approval row and any queued messages so the next mention restarts the flow.
Files back to Slack (send_file_to_user)
Skills can call the send_file_to_user(local_path, title?, comment?) MCP tool to upload an artifact to the current thread via files.upload_v2. Typical pattern: a skill produces a PDF / image / CSV with Bash, then attaches it. No special wiring per skill.
Progress hourglass
While Claude thinks or runs tools but hasn't produced text yet, jean alternates the ⏳ and ⌛ reactions on the user's message at a 4-second cadence (under Slack's reactions rate limit). Stops as soon as the first content lands.
Owner CC
When a non-owner mentions jean in a channel for the first time in a thread, jean posts _cc <@owner>_ so the owner is notified. One per thread; suppressed if the owner has already participated.
Architecture
- Process model. Single Python process, single Fly machine, single uvicorn worker. SQLite + WAL on a Fly persistent volume at
/data. - Slack transport. Socket Mode. No public webhook URL for events; only the SIWS callback is browser-side HTTP.
- Per-thread Claude sessions. One
ClaudeSDKClientper(channel, thread_ts)while warm; idle-reaped afteridle_timeout_sec(default 5 min) and resumed via the SDK's on-disk transcript on next mention. - Web UI. FastAPI + Jinja2 + Pico.css. Authenticated via Sign in with Slack (OIDC).
- Container.
python:3.12-slim+ Node.js + poppler-utils + LibreOffice + gosu. Runs as non-rootjean(uid 1000); entrypoint chowns/dataand symlinks/home/jean/.claude → /data/home/.claudeso SDK transcripts land on the volume.
Development
pip install -e .[dev]
pytest # 51 tests, ~0.7s
pytest -k "not import" skips the smoke import test (which requires runtime deps installed).
See AGENTS.md for architectural invariants and gotchas before changing the harness, handlers, or Docker bits.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jean_agent-0.1.0.tar.gz.
File metadata
- Download URL: jean_agent-0.1.0.tar.gz
- Upload date:
- Size: 100.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cd220ffc6a80c5be48d173c3304b42964e8e9228acd0a296b5784aa6483d8452
|
|
| MD5 |
9da40d846f9d0e702db7d4690683dc69
|
|
| BLAKE2b-256 |
d779683604723e6503dd4f1ea1061bbc16c758f4b79f6e4e1557fb6aa2af7f9b
|
Provenance
The following attestation bundles were made for jean_agent-0.1.0.tar.gz:
Publisher:
release.yml on shinkansenfinance/jean
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jean_agent-0.1.0.tar.gz -
Subject digest:
cd220ffc6a80c5be48d173c3304b42964e8e9228acd0a296b5784aa6483d8452 - Sigstore transparency entry: 1769200200
- Sigstore integration time:
-
Permalink:
shinkansenfinance/jean@532ade6cbe535e9c0c4772ceded8c25cc5a731e1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/shinkansenfinance
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@532ade6cbe535e9c0c4772ceded8c25cc5a731e1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file jean_agent-0.1.0-py3-none-any.whl.
File metadata
- Download URL: jean_agent-0.1.0-py3-none-any.whl
- Upload date:
- Size: 107.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7875e7727483d0f8636f7c50746c0a5381ab9e252e31463bd75b9283a8be94b4
|
|
| MD5 |
eab12779c9981d73af1157703b1285e3
|
|
| BLAKE2b-256 |
fd6f6e773056fd7c0057e4c1bd104db0eb3eb75eeecfa0d8ce44a166b65a9ea6
|
Provenance
The following attestation bundles were made for jean_agent-0.1.0-py3-none-any.whl:
Publisher:
release.yml on shinkansenfinance/jean
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jean_agent-0.1.0-py3-none-any.whl -
Subject digest:
7875e7727483d0f8636f7c50746c0a5381ab9e252e31463bd75b9283a8be94b4 - Sigstore transparency entry: 1769200559
- Sigstore integration time:
-
Permalink:
shinkansenfinance/jean@532ade6cbe535e9c0c4772ceded8c25cc5a731e1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/shinkansenfinance
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@532ade6cbe535e9c0c4772ceded8c25cc5a731e1 -
Trigger Event:
push
-
Statement type: