Skip to main content

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 @mention it 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 bundled skill-builder skill 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 Read them; skills can call send_file_to_user to 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.update so 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/users for governance.
  • Self-diagnosing. uvx jean-agent doctor --setup-guide walks you through every secret with the Slack manifest inlined; uvx jean-agent doctor --deep runs 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 uv or the upstream installer)
  • A skills/<name>/SKILL.md somewhere in the repo where you'll run jean (or just create one as you go — the bundled skill-builder skill 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-agent puts both jean-agent and jean on your PATH; both invoke the same Typer app. Everywhere this README says uvx jean-agent <cmd> you can run jean <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-root jean user.
  • .dockerignore
  • jean_system_prompt.md — the host's optional system prompt.

Dockerfile. The generated Dockerfile runs pip install jean-agent with a version constraint matching whatever jean version you ran init with (e.g. 0.1.x → jean-agent>=0.1.0,<0.2.0). To upgrade: bump your local jean (uv tool upgrade jean-agent) then re-run uvx jean-agent init --force to regenerate the Dockerfile with the new constraint — or just edit it by hand. For working off local unreleased changes (e.g. testing a PR before publishing), run uvx jean-agent vendor to snapshot the installed jean into .jean-vendor/jean/ and swap the install line to RUN pip install /app/.jean-vendor/jean.

Releases

jean-agent follows semantic versioning. Releases live at pypi.org/project/jean-agent.

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/appsCreate 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:

  1. Install App → copy the Bot User OAuth Token (xoxb-…)
  2. Basic Information → App-Level Tokens → Generate one with connections:write scope; copy the xapp-… token
  3. 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:

  1. Check flyctl auth whoami
  2. Read fly.toml (app name, region, volume name)
  3. Read encrypted credentials and push them as Fly secrets (staged for the deploy)
  4. Create the app if missing
  5. Create the persistent volume (jean_data, 3 GB default) if missing
  6. 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.

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.

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_file writes it to /data/skills/standup/SKILL.md and bumps the metadata in SQLite.

Skills can be multi-file. Editing works through the same surface (list_user_skillsread_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):

  1. 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.
  2. The event gets queued in pending_channel_messages.
  3. Owner receives a DM with Block Kit Approve / Deny buttons.
  4. On approve: queued messages are replayed through the normal handler — the original asker gets their answer.
  5. 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 ClaudeSDKClient per (channel, thread_ts) while warm; idle-reaped after idle_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-root jean (uid 1000); entrypoint chowns /data and symlinks /home/jean/.claude → /data/home/.claude so 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jean_agent-0.2.0.tar.gz (109.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jean_agent-0.2.0-py3-none-any.whl (114.2 kB view details)

Uploaded Python 3

File details

Details for the file jean_agent-0.2.0.tar.gz.

File metadata

  • Download URL: jean_agent-0.2.0.tar.gz
  • Upload date:
  • Size: 109.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for jean_agent-0.2.0.tar.gz
Algorithm Hash digest
SHA256 3932b02022c5f6a210205c06dd962eaf8958abf9abca1f9743175e16d2f2b479
MD5 e2fd264d5448dd63ffa7a1b913a12d85
BLAKE2b-256 29f423cf541bd356a79fe55c0baeb0a14c13e40bcd6aef896e92882e1cfb8e77

See more details on using hashes here.

Provenance

The following attestation bundles were made for jean_agent-0.2.0.tar.gz:

Publisher: release.yml on shinkansenfinance/jean

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file jean_agent-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: jean_agent-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 114.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for jean_agent-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 01ab7975fa55475cb108dd6f3050e4d999940ff699e5b6d91ff80703c84b049b
MD5 6396a8b566b0da1fd35869ed95b7c724
BLAKE2b-256 103411d2e846abf820e08b7f81d3c1c633bca052de4c229184eb0ed74028f2c5

See more details on using hashes here.

Provenance

The following attestation bundles were made for jean_agent-0.2.0-py3-none-any.whl:

Publisher: release.yml on shinkansenfinance/jean

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page