Back to Blog

I Hired an AI Chief of Staff. His Name Is Bleu.

He lives on a Mac mini, listens on Telegram, sweeps GitHub and Vercel, and opens PRs against himself overnight.

Sean Robb
8 min read
#ai

A few weeks ago I wrote about Austin Lau, the one-person growth team at Anthropic. One marketer plus the right toolkit, beating teams that didn’t have the toolkit. I keep coming back to that idea, because I’m a solo founder who can’t hire a team but can hire software that acts like one. So I hired Bleu.

WorkHoodie
Bleu
Bleu
AI Chief of Staff

He has a name, a job description in a text file, a schedule in a markdown file, and a Mac mini for a desk. His logins are separate from mine and scoped narrower, the same reason a real employee gets their own accounts.

He shows up every morning, sweeps the repos and dashboards, pings me on Telegram when something needs a human, and otherwise stays out of my way. The whole “AI chief of staff” is a markdown file, a bash script, and a long-running Claude Code session on a Mac mini under my desk, with no SaaS or infra in front of it.

The System

A handful of files do the work.

bleu/
├── bootstrap.sh              ← kills old sessions, pulls main, launches tmux
├── schedule.md               ← one heading per recurring job
└── .bleu/
    ├── bleu-prompt.txt       ← the job description
    └── run-bleu.sh           ← invokes the claude CLI with Telegram enabled

A macOS LaunchAgent (Apple’s cron equivalent — runs scripts on a schedule or at login) fires bootstrap.sh at login and again at 3am every day. The script keeps the Mac awake, pulls latest, kills any old sessions, and launches a fresh one:

# Prevent macOS from sleeping — keeps tmux sessions alive
if ! tmux has-session -t bleu-caffeinate 2>/dev/null; then
  tmux new-session -d -s bleu-caffeinate 'caffeinate -i -s'
fi

# Kill old sessions, launch the unified Bleu session
for session in bleu bleu-scheduler bleu-telegram; do
  tmux has-session -t "$session" 2>/dev/null && tmux kill-session -t "$session"
done
tmux new-session -d -s bleu "bash $BLEU_DIR/.bleu/run-bleu.sh"

(tmux is what lets the claude process keep running after I close the terminal — Bleu lives inside one of these sessions all day.)

run-bleu.sh is where Bleu actually starts up. Anthropic ships an official Telegram channel plugin that turns a Telegram chat into a first-class input for Claude Code. One CLI flag — enableRemoteControl — and the long-running session is listening on my phone:

claude \
  --settings '{"enabledPlugins":{"telegram@claude-plugins-official":true},"enableRemoteControl":true}' \
  --channels 'plugin:telegram@claude-plugins-official' \
  --name bleu \
  "$(cat .bleu/bleu-prompt.txt)"

That flag makes Telegram a two-way channel: Bleu’s input and his permission prompts. When he wants to do something destructive, he asks me on my phone first and I tap yes or no. Routine reads and writes don’t ping.

The kick-off prompt tells the session who it is (“you are Bleu, the unified session, both Telegram listener and scheduler”) and points it at schedule.md. One heading per recurring job, with a cron expression and a prompt. Here’s how Morning Update opens:

## Morning Update
- **Cron:** `3 8 * * 1-5` (weekdays 8:03am CT)
- **Prompt:**

You are Bleu, AI Chief of Staff for WorkHoodie. Send Sean a morning update.

Check across every repo: open issues assigned to Bleu, PRs awaiting
review, Vercel deployments, PostHog errors and feature flags, blog
readership, Meta campaign performance, Supabase health, competitor news.

On boot, the kick-off prompt sends Bleu through schedule.md, where he registers each job with CronCreate (Claude Code’s built-in scheduler) before he says hello.

Every job inherits the same default rule: stay silent if nothing’s wrong. The schedule pages me when there’s signal, not when there’s a check.

Boot — three minutes after the Mac wakes up
>_ Mac mini Bleu Telegram Me LaunchAgent fires bootstrap.sh tmux new-session 'claude --channels telegram' reads schedule.md, registers CronCreate jobs Bleu online ping

Two Ways In

One on the schedule, one from me.

Scheduled

When a cron fires, the work falls into three buckets, all the kind a solo dev keeps meaning to do but never gets around to.

  • Code in flight. Sweep stale PRs and unaddressed review comments. Triage new issues into “now” versus “later.”
  • Production health. Watch deploys for errors and rollbacks. Scan product analytics for spikes or regressions. Audit dependencies for security advisories.
  • The big picture. Skim competitor and industry news. Review the day’s commits and codify patterns worth keeping.

Two jobs are worth pulling out.

Morning Update. At 8:03am every weekday, Bleu does his rounds across every repo and dashboard, then lands a summary on my phone before I open the laptop. If everything’s clean I get one line, otherwise a paragraph.

Nightly Reflection. At 10:03pm, Bleu looks back through the day’s commits and review comments, and if he spots a pattern worth keeping, he opens a PR against his own setup. I merge it in the morning. The agent updates the agent.

Nightly Reflection — the agent updates the agent
Bleu GitHub Me 10:03pm cron fires reflection read today's commits and PR comments open draft PR with new conventions PR for review (next morning) merge

Inbound

Me texting Bleu. He decides whether to handle it inline or spawn a background agent. The agent works, opens a PR, and pings me on Telegram when it’s done.

Inbound — I text Bleu, Bleu hands it off
Me Telegram Bleu Background agent GitHub fix PR #123 review comments deliver spawn background agent address comments, push commits PR #123 ready ping

Try It Yourself

A few things worth knowing before you copy this.

Claude Code’s CronCreate is session-scoped and recurring tasks expire after seven days, which is why the LaunchAgent re-fires bootstrap.sh every morning at 3am. The daily restart is what keeps the schedule alive. It looked like a workaround the first time I set it up, but after a week of running it, it’s the cleanest part of the design. The schedule lives in cron, the source of truth lives in schedule.md, and the daily restart reconciles them.

The Telegram plugin needs a bot token. The Bot API is free and the setup is a five-minute chat with @BotFather. Bleu pulls the token from ~/.claude/channels/telegram/.env so scheduled jobs can also send messages directly via curl.

The bigger risk than any single tool call is the kick-off prompt and schedule.md themselves. They’re the job description, and a sloppy line gets run unattended hundreds of times. Be deliberate about what you tell him to do, and keep the schedule in version control so every change goes through a PR.

If you want to set up your own version, paste this into Claude Code from an empty directory. It’ll ask you what to call your agent, inspect your stack, scaffold the bootstrap script, run script, kick-off prompt, a starter schedule.md, and the LaunchAgent plist, then walk you through launchctl load.

I want to set up an AI chief of staff that runs on a Mac as a long-lived Claude Code session, listens on Telegram, and runs scheduled jobs against my GitHub org and dashboards. The pattern: a single tmux session running 'claude' with the official Telegram channel plugin, a schedule.md that defines recurring jobs via CronCreate, and a daily LaunchAgent that re-runs a bootstrap script to keep the cron alive. First, ask me four questions before generating anything: 1. What name should I give the agent? (used for tmux session name, plist name, file paths — e.g. "alfred", "robin", "ada") 2. What's the Mac's username and the absolute path the repo should live at? 3. What's my GitHub org or username, and which repos should the agent watch?
I want to set up an AI chief of staff that runs on a Mac as a long-lived Claude Code session, listens on Telegram, and runs scheduled jobs against my GitHub org and dashboards. The pattern: a single tmux session running 'claude' with the official Telegram channel plugin, a schedule.md that defines recurring jobs via CronCreate, and a daily LaunchAgent that re-runs a bootstrap script to keep the cron alive. First, ask me four questions before generating anything: 1. What name should I give the agent? (used for tmux session name, plist name, file paths — e.g. "alfred", "robin", "ada") 2. What's the Mac's username and the absolute path the repo should live at? 3. What's my GitHub org or username, and which repos should the agent watch? 4. What dashboards should the agent sweep? (Vercel project names, PostHog project ID + API key, Supabase project refs, anything else) Use {NAME} as the agent's name in everything below. Once I answer, scaffold the following files in the current directory: 1. bootstrap.sh — Bash script that: - Sets PATH so cron can find claude, gh, brew, etc. - Starts a '{NAME}-caffeinate' tmux session running 'caffeinate -i -s' if not already running - Runs 'brew upgrade claude-code' (ignore failures) - cd's to the repo, checks out main, and pulls --ff-only - Kills any existing '{NAME}' tmux session - Starts a fresh '{NAME}' tmux session running '.agent/run.sh' 2. .agent/run.sh — Bash script that: - Sources $HOME/.claude/channels/telegram/.env so scheduled jobs can curl the Bot API - Runs: claude --settings '{"enabledPlugins":{"telegram@claude-plugins-official":true},"enableRemoteControl":true}' --channels 'plugin:telegram@claude-plugins-official' --name {NAME} "$(cat .agent/prompt.txt)" 3. .agent/prompt.txt — The kick-off prompt. Tell the session who it is (its name, that it's an AI chief of staff for the user) and that it's the unified Telegram listener and scheduler. It should append a "Sent by {NAME}" suffix to outbound Telegram messages so the user can tell at a glance that it came from the agent. Any computer-use work must be delegated to a background sub-agent — never run inline, it would block the listener. On boot, read schedule.md, create all CronCreate jobs, then send an "online" Telegram message. Include the Telegram chat_id from my answer. 4. schedule.md — A starter calendar with three jobs: a Morning Update (weekdays 8am, sweeps the repos + dashboards I named), a PR & Issue Sweep (every 2 hours during business hours, spawns background agents for anything actionable), and a Nightly Reflection (10pm, reviews the day's commits and PR comments, opens a draft PR against this repo to codify any reusable learnings). Each job is a heading with a Cron line and a Prompt block. Default rule for every job: stay silent unless there's signal. 5. com.{ORG}.{NAME}.plist — LaunchAgent that runs bootstrap.sh at login and daily at 3am. Use the username and path from my answers. Include StandardOutPath and StandardErrorPath so I can debug when things go wrong. 6. .claude/agents/ — Drop in starter prompt templates for pr-feedback.md, issue-worker.md, ci-fixer.md, and rebaser.md. Each is a one-page prompt the unified session can hand to a background agent. After scaffolding, print exactly the commands I need to run to install: cp com.{ORG}.{NAME}.plist ~/Library/LaunchAgents/ launchctl load ~/Library/LaunchAgents/com.{ORG}.{NAME}.plist bash bootstrap.sh # to start it the first time without rebooting Stop and ask me before installing anything yourself.

A few weeks in, the wins are quieter than I expected, and most of them start on Telegram. I’ll text Bleu a half-formed idea — “we should add X to the changelog” or “the pricing FAQ is stale” — and he turns it into a scoped GitHub issue with acceptance criteria. The small ones he picks off himself: spawn a background agent, push a branch, open a PR. By the time I’m back at my laptop the work is waiting for review.

The morning sweeps catch the rest — competitor moves and traffic shifts I wouldn’t have thought to check that day. Bleu has his own scoped credentials and his own opinions about my codebase by the time he reflects on it at night. He’s the always-online employee I never managed to hire.

The reflection PRs show up overnight, and I keep forgetting he’s there until he pings me.