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 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 @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. jean doctor --setup-guide walks you through every secret with the Slack manifest inlined; jean 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 stays in its own private repo and you invoke it via uvx (npx-equivalent for Python, ships with uv).

Prerequisites:

  • uv installed (brew install uv or the upstream installer)
  • Your GitHub SSH key configured (jean lives in a private repo; uvx clones via git+ssh)
  • 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. 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-root jean user.
  • .dockerignore
  • jean_system_prompt.md — the host's optional system prompt.

Dockerfile + private repo. The generated Dockerfile installs jean from ./.jean-vendor/jean/, a directory that jean deploy populates automatically by calling jean vendor right before fly 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, run jean vendor by hand once, then docker build as 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/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

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:

  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.

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_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.1.0.tar.gz (100.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.1.0-py3-none-any.whl (107.4 kB view details)

Uploaded Python 3

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

Hashes for jean_agent-0.1.0.tar.gz
Algorithm Hash digest
SHA256 cd220ffc6a80c5be48d173c3304b42964e8e9228acd0a296b5784aa6483d8452
MD5 9da40d846f9d0e702db7d4690683dc69
BLAKE2b-256 d779683604723e6503dd4f1ea1061bbc16c758f4b79f6e4e1557fb6aa2af7f9b

See more details on using hashes here.

Provenance

The following attestation bundles were made for jean_agent-0.1.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.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

Hashes for jean_agent-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7875e7727483d0f8636f7c50746c0a5381ab9e252e31463bd75b9283a8be94b4
MD5 eab12779c9981d73af1157703b1285e3
BLAKE2b-256 fd6f6e773056fd7c0057e4c1bd104db0eb3eb75eeecfa0d8ce44a166b65a9ea6

See more details on using hashes here.

Provenance

The following attestation bundles were made for jean_agent-0.1.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