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 uvx jean-agent 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.
uvx jean-agent doctor --setup-guidewalks you through every secret with the Slack manifest inlined;uvx jean-agent 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 ships on PyPI as jean-agent;
invoke it without installing via uvx (npx-equivalent for Python).
Prerequisites:
- uv installed (
brew install uvor the upstream installer) - 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. Scaffold jean.toml + fly.toml + Dockerfile into the current directory
uvx jean-agent init # interactive: Fly app name, owner, bot display name
# 2. Walk through every secret (Anthropic + 4 Slack values), browser-open + clipboard
uvx jean-agent configure-secrets
# 3. Verify everything is wired up
uvx jean-agent doctor --deep # workspace identity, owner, scopes
# 4. Provision Fly app + volume + secrets, then deploy
uvx jean-agent deploy
Prefer a shorter command?
uv tool install jean-agentputs bothjean-agentandjeanon your PATH; both invoke the same Typer app. Everywhere this README saysuvx jean-agent <cmd>you can runjean <cmd>.
What uvx jean-agent 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. Two things are auto-managed by
upgrade-target:
- Version pin:
pip install 'jean-agent>=X.Y.Z,<X.(Y+1).0'. Floor moves with every release (one-line diff to commit), ceiling caps at next minor (rebuilds within a minor pick up patches automatically; minor bumps need an explicitupgrade-target).- apt block between
# jean-apt-start/# jean-apt-endmarkers. Rewritten end-to-end on everyupgrade-targetfrom jean's canonical runtime deps (bubblewrap,socat,libreoffice, etc) PLUS whatever you list in[jean] custom_apt_packagesinjean.toml. Don't edit between the markers — editjean.toml.Everything outside the markers — base image, non-root user setup, entrypoint — is yours. For working off local unreleased changes (e.g. testing a PR before publishing), run
uvx jean-agent vendorto snapshot the installed jean into.jean-vendor/jean/and swap the install line toRUN pip install /app/.jean-vendor/jean.
Releases
jean-agent follows semantic versioning. Releases live at
pypi.org/project/jean-agent.
Release notes live in CHANGELOG.md (Keep a Changelog
format). After every deploy, jean reads the delta between the version
it last ran with and the version it's running now, and DMs the owner a
summary of what's new. The same content renders at /admin/changelog
in the web UI, and uvx jean-agent upgrade-target prints it in the
terminal during local upgrades. The release workflow refuses to publish
a tag that has no matching CHANGELOG section, so the changelog can't
silently drift out of sync.
The release flow (maintainers only):
# 1. Bump version in pyproject.toml (e.g., 0.1.0 → 0.2.0) and commit on main
# 2. Tag + push — the GitHub Actions workflow takes it from there
git tag v0.2.0
git push origin v0.2.0
The workflow at .github/workflows/release.yml validates that the tag matches
pyproject.toml's version, builds the wheel + sdist, and publishes to PyPI via
Trusted Publishing — no API tokens
stored in repo secrets. Publishing rights are bound to (repo + workflow filename
- environment) on the PyPI side.
The Dockerfile template (jean/templates/Dockerfile.tmpl) does NOT hardcode
a version — init computes the constraint at scaffold time from the version
of jean the user is running, so bumping pyproject.toml automatically flows
through to the next uvx jean-agent init for downstream users.
Existing installs upgrade in one step:
uvx jean-agent upgrade # detects uv tool / pip and runs the right install,
# then re-invokes upgrade-target so the NEW jean
# refreshes the Dockerfile pin + README/AGENTS jean
# section + .github/workflows/jean-deploy.yml
"Target-repo updates" (what upgrade-target runs) are distinct from
database migrations (what jean migrate and app startup apply to
SQLite). See AGENTS.md § "Two distinct update pipelines" for the
disambiguation.
Setting up the Slack app
Skip manual scope-clicking — let jean print the manifest:
uvx jean-agent 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
uvx jean-agent 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
uvx jean-agent 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. |
[claude] |
sandbox_bash |
true |
Run the SDK's Bash tool inside bubblewrap with no network. Flip to false only if a skill needs outbound HTTP from Bash. |
[claude] |
excluded_bash_commands |
[] |
Commands the sandbox should leave alone (e.g. ["soffice"] if a binary doesn't tolerate bwrap's namespacing). |
[claude] |
use_anthropic_proxy |
true |
Route SDK API calls through a local proxy so the real sk-ant-… key never enters the subprocess env. See Security below. |
[jean] |
custom_apt_packages |
[] |
Extra OS packages for your skills (e.g. ["imagemagick", "tesseract-ocr"]). Composed into the Dockerfile's apt block on the next upgrade-target. Don't list jean's own deps. |
[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.
uvx jean-agent 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 — uvx jean-agent deploy reads from the local store and pushes them as Fly secrets in one shot.
Security
A few defenses worth knowing about — the operator doesn't have to do anything to enable them, they're on by default:
- Real
ANTHROPIC_API_KEYnever enters the SDK subprocess. A local HTTP proxy bound to127.0.0.1:<random>holds the real key; eachLiveSessionmints an ephemeraljean_<random>token that the SDK sees asANTHROPIC_API_KEY. The proxy validates the token, swaps to the real key, and forwards toapi.anthropic.com. Tokens revoke when the session closes. ABash("env")exfil only ever leaks a short-lived token bounded to the session. Disable with[claude] use_anthropic_proxy = falseif you need to debug API errors without the hop. - Bash sandbox via
bubblewrapwith no network. Even if a skill exfils env, the follow-upcurl evilcan't leave the container.[claude] sandbox_bash = falseopts out;excluded_bash_commands = ["soffice"]exempts specific binaries. - Env scrub. The SDK subprocess inherits only an allow-list:
ANTHROPIC_*,HOME,PATH,LANG,LC_*,TZ,NODE_OPTIONS. Slack tokens, the session secret, and any operator-set env vars are stripped before the SDK starts. - Encrypted credentials store uses AES-256-GCM bound to the
project's path (so a blob from project A can't decrypt under
project B's key). Master key in the OS keychain via
keyring.
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.13.1.tar.gz.
File metadata
- Download URL: jean_agent-0.13.1.tar.gz
- Upload date:
- Size: 203.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
58bb6a160976b6b3f632cb0b4e7c5c6d4c0248d0d2914010ec9cd626356b848d
|
|
| MD5 |
716046e2678c4c616419b424d2d16c6c
|
|
| BLAKE2b-256 |
f26329898705c7e496f33e61b0d831e84b82af76234fa921abdb0d1ee237d728
|
Provenance
The following attestation bundles were made for jean_agent-0.13.1.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.13.1.tar.gz -
Subject digest:
58bb6a160976b6b3f632cb0b4e7c5c6d4c0248d0d2914010ec9cd626356b848d - Sigstore transparency entry: 1774470821
- Sigstore integration time:
-
Permalink:
shinkansenfinance/jean@1f7549de609ac1ed29507948e0ee18febbff3306 -
Branch / Tag:
refs/tags/v0.13.1 - Owner: https://github.com/shinkansenfinance
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1f7549de609ac1ed29507948e0ee18febbff3306 -
Trigger Event:
push
-
Statement type:
File details
Details for the file jean_agent-0.13.1-py3-none-any.whl.
File metadata
- Download URL: jean_agent-0.13.1-py3-none-any.whl
- Upload date:
- Size: 180.7 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 |
b429ad7e8a04059db3614ed86d686bec246d062a483f38b324cb3b3b6bf05fe1
|
|
| MD5 |
8d709d0f43ff1207ec31ffcc188d7a26
|
|
| BLAKE2b-256 |
14d63bc74e0f8130702d04f2b54db70828fcea4e0ec42e5ea1256bd4e0bef0b3
|
Provenance
The following attestation bundles were made for jean_agent-0.13.1-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.13.1-py3-none-any.whl -
Subject digest:
b429ad7e8a04059db3614ed86d686bec246d062a483f38b324cb3b3b6bf05fe1 - Sigstore transparency entry: 1774470904
- Sigstore integration time:
-
Permalink:
shinkansenfinance/jean@1f7549de609ac1ed29507948e0ee18febbff3306 -
Branch / Tag:
refs/tags/v0.13.1 - Owner: https://github.com/shinkansenfinance
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1f7549de609ac1ed29507948e0ee18febbff3306 -
Trigger Event:
push
-
Statement type: