Discord Rich Presence for Claude Code and Codex.
Show what your coding agent is doing — live on your Discord profile.
┌──────────────────────────────────────────┐
│ ▣ Claude Code │
│ Editing code · Opus 4.8 │
│ 12:34 elapsed │
└──────────────────────────────────────────┘
A single static binary. No Node, no Python, no bot token.
Existing Discord presence tools for Claude Code are Node or Python scripts that only
cover one agent, and often only one OS. agent-presence is one ~1.5 MB binary that:
- supports both Claude Code and Codex from one daemon
- runs on macOS, Windows and Linux
- adds ~6 ms per agent event, and is a strict no-op if anything goes wrong
- shows nothing identifying by default — see Privacy
# Homebrew (macOS)
brew install jx-grxf/tap/agent-presence
# Scoop (Windows)
scoop bucket add jx-grxf https://github.com/jx-grxf/scoop-bucket
scoop install agent-presence
# Prebuilt binary (macOS arm64/x64, Linux x64, Windows x64)
# https://github.com/jx-grxf/agent-presence/releases/latest
# From source
cargo install --git https://github.com/jx-grxf/agent-presenceClaude Code users can skip the hook wiring and take the plugin instead — the binary is
still needed, see plugin/:
/plugin marketplace add jx-grxf/agent-presence
/plugin install agent-presence@jx-grxf
Scoop wires up your agents during install. Homebrew cannot — a formula runs sandboxed and must not write outside its prefix — so on macOS and Linux, run it once:
agent-presenceagent-presence ● ● ○ ○ ○
│ How much should the card reveal?
│
│ ← generic →
│ Nothing identifying. No repository name, no branch, no file names.
│
│ This is what everyone on Discord would see:
│ Agent
│ Claude Code
│ Editing code · Opus 4.8
←→ change · enter continue · q quit
It detects which agents you have, asks the two questions that matter, installs the hooks, and reads the files back to confirm. Quitting before the install step leaves your machine exactly as it was.
That is the whole setup. The daemon starts itself with your next session and shuts down on its own once you stop coding.
To remove it completely:
agent-presence install --uninstallagent-presence updateIt works out what installed the binary — Homebrew, Scoop, cargo install, or a file you
put there yourself — runs that manager's own upgrade, and restarts the daemon on the new
build. Nothing self-overwrites: a Homebrew binary that replaced itself would leave the
Cellar disagreeing with the receipt and break every later brew upgrade. A standalone
install is the one case nothing can do for you, and it says so rather than guessing.
The restart is the part worth having. A running daemon holds the old binary open, so without it the card stays dark until you happen to open a new agent session.
agent-presence update --check reports what is available and installs nothing. The
daemon also asks GitHub once a day on its own and caches the answer, so status and
doctor can mention a new release without ever going to the network themselves:
Daemon
✓ running (pid 4821)
! v0.2.3 available — run `agent-presence update`
That check is the only thing update_check controls. Nothing installs itself.
Set update_check = false to switch it off.
The default card shows no repository name, no branch, and no file names. It says which agent is running, what kind of work it is doing, and how long you have been at it.
Anything more is opt-in. Either run the settings menu, which previews the card live as you change things:
agent-presence config Detail project
Show model on
Follow focus on
┌─ preview — visible to everyone on Discord ─┐
│ Agent │
│ agent-presence · main │
│ Editing code · Opus 4.8 │
└────────────────────────────────────────────┘
↑↓ move · ←→/space change · enter edit · s save · q quit
…or edit ~/.config/agent-presence/config.toml by hand — it stays the source of truth,
and the menu is only a front end for it:
# "generic" (default) · "project" · "full"
detail = "generic"
# Show the model name, e.g. "Opus 4.8"
show_model = true
# Always generic for these paths, whatever `detail` says
hidden_paths = ["~/work/**", "~/clients/**"]
# Your own Discord application, if you would rather not use the bundled one
client_id = ""
# Sessions silent this long are dropped (agents can die without saying goodbye)
idle_timeout = "15m"
# Turn the card off without uninstalling the hooks
enabled = true
# With several sessions live, show the one in the focused terminal window.
# Off = always show the most recently active session.
follow_focus = true
# Let the daemon ask GitHub once a day whether a newer release exists.
# It only reports; installing stays a manual `agent-presence update`.
update_check = true
# Up to two link buttons on the card
# [[buttons]]
# label = "GitHub"
# url = "https://github.com/jx-grxf/agent-presence"detail |
Line 1 | Line 2 |
|---|---|---|
generic |
Claude Code |
Editing code · Opus 4.8 |
project |
agent-presence · main |
Editing code · Opus 4.8 |
full |
agent-presence · main |
Editing code: main.rs · Opus 4.8 |
hidden_paths always wins. A repo matching it falls back to generic even at
detail = "full".
full never publishes free-form text. It reduces whatever the agent is doing to a
shape that cannot carry a secret:
| Agent runs | Card says |
|---|---|
git push origin main 2>&1 | tail -3 |
Running commands: git push |
STRIPE_KEY=sk_live_9f2 ./deploy.sh |
Running commands: deploy.sh |
curl -H "Authorization: Bearer …" https://api.acme.io |
Running commands: curl |
Edit /Users/you/clients/acme/billing.rs |
Editing code: billing.rs |
a web fetch of https://api.acme.io/v1?token=… |
Researching: api.acme.io |
| a web search | Researching — the query is never sent |
Command arguments are dropped wholesale, because that is where keys, hosts, paths and connection strings live. Paths keep only their file name, URLs only their host.
| Agent is… | Card says |
|---|---|
| processing your prompt | Thinking |
| writing files | Editing code |
| running shell commands | Running commands |
| reading or searching | Reading code |
| searching the web | Researching |
| running subagents | Delegating to subagents |
| waiting on a permission prompt | Waiting for approval |
| done, awaiting input | Idle |
Running several sessions at once? The card follows the terminal window you are
looking at, and appends +2 more for the rest. Switch windows and the card switches
with you. The elapsed timer spans your whole coding stretch, not just the newest session.
Focus following is macOS-only for now (Ghostty, iTerm2 and Terminal.app) and needs the
Automation permission macOS asks for the first time it queries your terminal. Deny
it, set follow_focus = false, or run anywhere else, and the card falls back to the most
recently active session.
Claude Code / Codex
│ lifecycle hook, JSON on stdin
▼
agent-presence hook short-lived · ~6 ms · always exits 0
│ local socket
▼
agent-presence daemon one instance · owns the Discord connection
│ SET_ACTIVITY over IPC
▼
Discord desktop client
Both agents already emit lifecycle hooks (SessionStart, PreToolUse, Stop, …) with
almost identical JSON payloads, so one parser handles both.
The daemon exists for two reasons: Discord allows exactly one activity per application
while you may run many sessions, and SET_ACTIVITY is rate limited to roughly 5 calls
per 20 seconds — far slower than hooks fire during a busy turn. The daemon coalesces
updates onto a 2-second tick.
Rich Presence needs no bot and no token. The connection is authenticated by the Discord desktop client you are already logged into. The only credential is a public Application ID.
The hook binary runs inside your agent's process tree, so it follows three rules:
- Never writes to stdout — Claude Code injects hook stdout into the model's context, so anything printed would become text the model reads.
- Always exits 0 — exit code 2 would block the agent's tool call outright.
- Never blocks — everything is bounded by a 250 ms timeout. A missing or wedged daemon costs a few milliseconds, never a stalled session.
If Discord is closed, the daemon keeps running and reconnects when it comes back.
| Command | Does |
|---|---|
agent-presence install |
Add hooks to Claude Code and Codex |
agent-presence install --uninstall |
Remove them again |
agent-presence config |
Edit settings in a menu, with a live card preview |
agent-presence status |
Daemon, config and Application ID |
agent-presence doctor |
Diagnose a card that is not appearing |
agent-presence update |
Upgrade through whatever installed the binary |
agent-presence update --check |
Report what is available, install nothing |
agent-presence stop |
Stop the daemon |
agent-presence debug-activity |
Push a test card, to check the Discord link |
Logs: ~/.config/agent-presence/agent-presence.log. Raise the level with
AGENT_PRESENCE_LOG=agent_presence=debug.
Rich Presence identifies itself with a Discord Application ID. This is a public value, not a secret — there is no bot user, no OAuth and no token, because the connection is authenticated by the Discord desktop client you are already signed into.
An application ID ships with the binary, so nothing here is required. Set up your own only if you want the card to carry your own name and artwork:
- Create an application at discord.com/developers/applications. Its name becomes the bold first line of the card.
- Under Rich Presence → Art Assets, upload two images keyed exactly
claudeandcodex. - Copy the Application ID into your config:
client_id = "1234567890123456789"Or export AGENT_PRESENCE_CLIENT_ID for a quick test without editing the config.
No card appears. Run agent-presence doctor. Discord must be the desktop app —
the browser client has no IPC socket. Check that Settings → Activity Privacy → Display
current activity as a status message is on.
Card is stuck. agent-presence stop; the next tool call starts a fresh daemon.
Card does not follow the focused window. macOS needs to have granted
agent-presence Automation access to your terminal — check System Settings → Privacy
& Security → Automation. Ghostty is matched by working directory rather than terminal
device, so two sessions in the same repo can tie.
Hooks not firing. Restart the agent after install — both read their hook config at
startup.
MIT