disclaw #
My very own claw. Super experimental and only really meant for me for now.
A personal Claude-style agent that lives on my Mac mini, talks back through Discord, and remembers things between conversations. Built on the pi agent harness.
Architecture #
Mac mini
└── docker compose
└── disclaw (one Node process)
├── Pi SDK ........... agent runtime + tools (bash, edit, read, write)
├── Discord client ... DM / mention triggered
├── Lane manager ..... one session per channel; persisted in .pi/; steers mid-turn
└── Scheduler ........ croner, fires fresh sessions on cron
+ volumes:
memory/ persistent markdown notes + scheduled jobs
.pi/ persistent Pi session files + lane index
skills/ read-only mount of skill instructions
Prerequisites #
- Node 20+ (for local dev) — Docker handles this in deployment
- Docker + Docker Compose (for deployment)
- An Anthropic API key (or another Pi-supported provider)
- A Discord application + bot token
Quick start — local CLI #
For testing memory and schedules before setting up Discord.
cp .env.example .env
# fill in ANTHROPIC_API_KEY
npm install
npm run dev:cli
You should see skills: ..., memory, schedule in the boot output. Try:
remember that I prefer Go over Python— writesmemory/<topic>.md, updatesINDEX.mdwhat do you remember about me?— re-reads INDEX.md and any relevant memory fileslist_schedules— exercises the schedule tool
/exit to quit.
Discord bot setup #
- https://discord.com/developers/applications → New Application.
- Bot tab → Reset Token → copy. Paste into
DISCORD_BOT_TOKENin.env. - Bot tab → Privileged Gateway Intents → enable MESSAGE CONTENT INTENT.
- OAuth2 → URL Generator → scope
bot, permissionsView Channel+Send Messages+Read Message History+Add Reactions. Open the generated URL and add the bot to a server you control. The bot must share a server with you for DMs to work — there is no DM-only path. - In Discord: User Settings → Advanced → enable Developer Mode. Right-click your own name → Copy User ID. Paste into
DISCORD_ALLOWED_USER_IDSin.env(comma-separated for multiple).
Run it:
npm run dev
The bot responds to DMs from allowed users and to @-mentions in any server channel it's been added to. All other messages are silently ignored. Each Discord channel/thread keeps its own Pi session history, and that history survives bot restarts via .pi/sessions/.
Mac mini deployment #
- Stop the mini from sleeping (cron won't fire if it does):
sudo pmset -a sleep 0 sudo pmset -a displaysleep 10 - Set
TZin.envso cron expressions match wall clock:TZ=America/Los_Angeles - Build and start:
docker compose up --build -d docker compose logs -f - Confirm
disclaw bot ready as ...andscheduler: N job(s) registeredin the logs.
Update after pulling new code:
docker compose up --build -d
Stop:
docker compose down
Memory #
The agent reads/writes plain markdown files in memory/. Format is documented in skills/memory/SKILL.md. Short version:
memory/INDEX.mdis always loaded; one-line pointers to every memory file.- Each topic is its own
memory/<topic>.mdwithname/description/typefrontmatter. - Types:
user,feedback,project,reference.
Edit memory files directly in your editor whenever you want — the agent picks up changes the next time it reads them.
Schedules #
Tell the agent things like "every Tuesday at 9am, summarize my Twitter feed" — it writes memory/schedules/<name>.md with a cron expression and prompt. The scheduler watches that directory and fires each job in a fresh agent session at its cron time, posting the result back to the Discord channel where you scheduled it.
Edit a schedule file directly to tweak the prompt or cron — the scheduler reloads on filesystem change.
Cron is standard 5-field (min hour dom mon dow):
| Expression | Means |
|---|---|
0 9 * * 2 |
Tuesday 9am |
0 9 * * * |
Daily 9am |
0 9 * * 1-5 |
Weekdays 9am |
*/30 * * * * |
Every 30 minutes |
Customization #
- Model: set
DISCLAW_MODEL(and optionallyDISCLAW_PROVIDER) in.env. Default isanthropic/claude-sonnet-4-6. - New skill: drop a directory under
skills/<name>/SKILL.mdwith frontmatter (name,description). Bind-mount means no rebuild — restart the container to pick up. - New tool: add a
defineToolinsrc/tools/, register it insrc/agent.ts→createSession.
File layout #
src/
├── agent.ts shared deps + session factory
├── bot.ts Discord client + allowlist + dispatch
├── cli.ts terminal mode for local testing
├── lanes.ts per-channel session map; steers if streaming
├── scheduler.ts croner-based scheduler over memory/schedules/
├── tools/schedule.ts schedule_task / list_schedules / cancel_schedule
└── util.ts Discord chunking + schedule file IO
skills/
├── memory/SKILL.md
└── schedule/SKILL.md
memory/ persistent (Docker bind-mount)
├── INDEX.md
└── schedules/ one .md per cron job
.pi/ persistent (Docker bind-mount)
├── lane-sessions.json channel/thread/cli lane -> Pi session file
└── sessions/ Pi JSONL session files for each lane
Troubleshooting #
- Bot doesn't respond. Check
DISCORD_ALLOWED_USER_IDSmatches your user ID exactly. Confirm MESSAGE CONTENT INTENT is enabled in the dev portal. In a server, the bot only responds to @-mentions, not all messages. Model not found.DISCLAW_MODELisn't in Pi's known list. Try the defaultclaude-sonnet-4-6, or checknode_modules/@earendil-works/pi-ai/dist/models.generated.js.- Scheduler not firing. Verify
docker compose logs disclawshowsN job(s) registered. ConfirmTZis correct andsudo pmset -g | grep sleepshowssleep 0. - Schedule fires but posts nothing visible. The destination channel ID is probably wrong or the bot lost access to the channel. Open
memory/schedules/<name>.mdand fix thedestinationfield. - Two messages collide on one channel. That's the lane-steering path: the second message is injected into the running turn. The bot reacts ⏳ on the original message to acknowledge.