Discord Rich Presence for Claude Code - Show your session on Discord

Show your Claude Code session on Discord! Display your current project, git branch, model, session time, token usage, and cost in real-time.
| Platform | Status |
|---|---|
| macOS (Apple Silicon) | ✅ Tested |
| macOS (Intel) | ⚠️ Untested |
| Linux (x64) | ⚠️ Untested |
| Linux (ARM64) | ⚠️ Untested |
| Windows (x64) | ✅ Tested |
| Windows (ARM64) | ⚠️ Untested |
Note: macOS Intel and Linux should work but haven't been verified. Please report problems.
Windows users: Requires Git Bash (included with Git for Windows) for automatic plugin hooks. Alternatively, run the PowerShell scripts manually (
scripts/start.ps1andscripts/stop.ps1). WSL won't work as Discord runs on the Windows host.
# Add the marketplace
claude plugin marketplace add tsanva/cc-discord-presence
# Install the plugin
claude plugin install cc-discord-presence@cc-discord-presence
That's it! The plugin will automatically start when you begin a Claude Code session and stop when you exit.
claude plugin marketplace update cc-discord-presence
claude plugin update cc-discord-presence@cc-discord-presence
Then start a new Claude Code session. The plugin's start script notices that the daemon in ~/.claude/bin/ is from an older release, stops it, downloads the matching one and starts it.
Upgrading from 1.0.4 or earlier:
claude plugin updateonly replaces the plugin's own files. The daemon binary lives in~/.claude/bin/and, before 1.0.5, was downloaded once and never updated, so you may still be running the daemon from your first install. Updating to 1.0.5 fixes this automatically on your next session start. If Discord still doesn't show your project afterwards, delete~/.claude/bin/cc-discord-presence-*and start a new session. If you had set up the statusline integration, it keeps working, but on current Claude Code it's no longer needed.
# Clone and build
git clone https://github.com/tsanva/cc-discord-presence.git
cd cc-discord-presence
go build -o cc-discord-presence .
# Run manually
./cc-discord-presence
The app reads session data from three sources, using the first one available:
The plugin ships a Claude Code hooks module (hooks/register.ts) that writes the session's project, model, context size and cost to ~/.claude/discord-presence-module.json using Claude Code's own figures. It runs in the terminal, the Claude desktop app and IDE extensions on Claude Code versions with hooks module support, and needs no setup. It updates when a session starts, when you send a prompt, after each tool call, after each response, and clears its data when the session ends.
On Claude Code versions without hooks modules, you can configure the statusline integration for the same accuracy. On current versions it isn't needed.
As a last resort, the app parses Claude Code's session files from ~/.claude/projects/. Cost is estimated from the model pricing table.
<a name="statusline-setup"></a>
Only needed on Claude Code versions without hooks module support.
Automatic Setup (Recommended):
Run the setup script (requires jq):
# Find your plugin directory and run setup
~/.claude/plugins/cache/*/cc-discord-presence/*/scripts/setup-statusline.sh
Or if you have the repo cloned:
./scripts/setup-statusline.sh
The setup script will:
statusline-wrapper.sh to ~/.claude/~/.claude/settings.json automatically~/.claude/statusline.shManual Setup: If you prefer, edit ~/.claude/settings.json:
{
"statusLine": {
"command": "~/.claude/statusline-wrapper.sh",
"type": "command"
}
}
Then copy scripts/statusline-wrapper.sh to ~/.claude/statusline-wrapper.sh.
Note: Restart Claude Code after setup for changes to take effect.
Check which data source is being used by viewing the daemon log:
cat ~/.claude/discord-presence.log
You'll see one of:
✓ Found active session: project-name (using hooks module) - Best accuracy, no setup✓ Found active session: project-name (using statusline data) - Same accuracy, for older Claude Code versions✓ Found active session: project-name (using JSONL fallback) - Working, but cost is estimated; update Claude Code or set up the statusline┌─────────────────────────────────┐
│ Clawd Code │
│ Working on: my-project (main) │
│ Opus 4.5 | 1.5M tokens | $0.1234│
│ 00:45:30 elapsed │
└─────────────────────────────────┘
# Build for current platform
go build -o cc-discord-presence .
# Cross-compile for all platforms
mkdir -p bin
GOOS=darwin GOARCH=arm64 go build -o bin/cc-discord-presence-darwin-arm64 .
GOOS=darwin GOARCH=amd64 go build -o bin/cc-discord-presence-darwin-amd64 .
GOOS=linux GOARCH=amd64 go build -o bin/cc-discord-presence-linux-amd64 .
GOOS=linux GOARCH=arm64 go build -o bin/cc-discord-presence-linux-arm64 .
GOOS=windows GOARCH=amd64 go build -o bin/cc-discord-presence-windows-amd64.exe .
Cost is calculated using current Claude API pricing (Dec 2025):
| Model | Input (per 1M tokens) | Output (per 1M tokens) |
|---|---|---|
| Opus 4.5 | $15.00 | $75.00 |
| Sonnet 4.5 | $3.00 | $15.00 |
| Sonnet 4 | $3.00 | $15.00 |
| Haiku 4.5 | $1.00 | $5.00 |
By default, this uses a shared Discord application ("Clawd Code"). If you want to use your own:
ClientID in main.goclaude plugin uninstall cc-discord-presence@cc-discord-presence
If you set up statusline integration, restore your original settings:
# Remove the wrapper script
rm ~/.claude/statusline-wrapper.sh
# Restore your original statusline in settings.json:
# Option 1: Point back to the default statusline.sh
jq '.statusLine.command = "~/.claude/statusline.sh"' ~/.claude/settings.json > ~/.claude/settings.json.tmp \
&& mv ~/.claude/settings.json.tmp ~/.claude/settings.json
# Option 2: Remove statusline config entirely
jq 'del(.statusLine)' ~/.claude/settings.json > ~/.claude/settings.json.tmp \
&& mv ~/.claude/settings.json.tmp ~/.claude/settings.json
Restart Claude Code after making changes.
This application runs entirely locally and does not collect any data. See PRIVACY.md for details.
MIT License - see LICENSE for details.
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
Before submitting a PR, make sure to run the test suite:
go test -v ./...
hooks/register.ts 91 lines1import type { Engine, Register } from 'claude-code'
2
3// The daemon reads ~/.claude/discord-presence-module.json before the
4// statusline wrapper's file, so this module is the main data source wherever
5// Claude Code loads it (the desktop app, the terminal, IDE extensions). The
6// statusline wrapper remains the fallback for versions without hooks modules.
7const FILE = '.claude/discord-presence-module.json'
8
9// "claude-opus-5-5" -> "Opus 5.5", the form the statusline's display_name has.
10function displayName(id: string) {
11 const m = id.match(/^claude-([a-z]+)-(\d+)-(\d+)/)
12 return m ? `${m[1][0].toUpperCase()}${m[1].slice(1)} ${m[2]}.${m[3]}` : id
13}
14
15// HOME is usually unset on Windows; the daemon resolves the same folder.
16async function filePath($: Engine) {
17 const [home, userProfile] = await Promise.all([$.env.get('HOME'), $.env.get('USERPROFILE')])
18 const dir = home || userProfile
19 return dir ? `${dir}/${FILE}` : undefined
20}
21
22async function publish($: Engine) {
23 const [path, id, cwd, root, model, usage, now] = await Promise.all([
24 filePath($),
25 $.session.id(),
26 $.session.cwd(),
27 $.session.root(),
28 $.session.model(),
29 $.session.usage(),
30 $.clock.now(),
31 ])
32 if (!path) return
33
34 // Same shape as the statusline JSON, so the daemon parses both alike.
35 const data = {
36 session_id: id,
37 cwd,
38 model: { id: model, display_name: displayName(model) },
39 workspace: { current_dir: cwd, project_dir: root },
40 cost: {
41 total_cost_usd: usage.cost?.usd ?? 0,
42 total_duration_ms: now - usage.startedAt,
43 },
44 // Context size: the last request's input, cached tokens included.
45 context_window: { total_input_tokens: usage.context.tokens ?? 0 },
46 }
47 await $.fs.write(path, JSON.stringify(data))
48}
49
50// Hooks cannot delete files, so an ending session blanks the file to "{}",
51// which the daemon reads as no data, but only when the file is still its own.
52async function clear($: Engine) {
53 const [path, id] = await Promise.all([filePath($), $.session.id()])
54 if (!path) return
55 const current = await $.fs.read(path).catch(() => '')
56 if (current.includes(`"session_id":"${id}"`)) await $.fs.write(path, '{}')
57}
58
59export const register: Register = on => {
60 on('session.start', async ($, e, next) => {
61 const result = await next(e)
62 await publish($).catch(() => {})
63 return result
64 })
65
66 // Discord switches to this session as soon as a prompt is sent.
67 on('prompt.submit', async ($, e, next) => {
68 await publish($).catch(() => {})
69 return next(e)
70 })
71
72 // Keeps the figures moving during long turns; the daemon only pushes to
73 // Discord when the displayed text changes.
74 on('tool.call', async ($, e, next) => {
75 const result = await next(e)
76 await publish($).catch(() => {})
77 return result
78 })
79
80 on('turn.complete', async ($, e, next) => {
81 const result = await next(e)
82 if (e.agentId === undefined) await publish($).catch(() => {})
83 return result
84 })
85
86 on('session.end', async ($, e, next) => {
87 await clear($).catch(() => {})
88 return next(e)
89 })
90}
91