Goated is an always-on personal AI assistant, built around Claude Code and Codex. It's minimal, performant, and piggybacks on the best harnesses in the world for long-running sessions.
Written in golang, with cobra + viper + bbolt + tmux + crontab.
Why Goated vs. OpenClaw?
Well, besides the fact that many (most?) OpenClaw users are violating Claude Code's TOS by hijacking their Max credentials: Goated is also simply faster, smaller, and much more performant.
This is because most agent frameworks own the context window — they inject bootstrap files, manage session history, and accumulate state in-process until memory explodes. Goated doesn't touch the context window at all. It's a ~20 MB daemon that pastes message envelopes into tmux and lets Claude Code, Codex, or Pi handle its own context compaction, memory, and token budgeting. The result: no token bloat, no session file growth, no multi-GB memory leaks. Just a thin orchestrator that stays out of the way. See docs/PERFORMANCE.md for the full comparison.
Out of the box, Goated supports:
- Slack and Telegram chat interfaces
- Claude Code and Codex in both headless and TUI modes
- Pi in headless mode
- Long-running daemon operation with watchdog recovery
- Obsessive, automatic Obsidian-style notetaking with the excellent
notesmdCLI - Cron jobs and headless subagents
- File-backed credential management
- Session health checks, restart handling, and queueing
- A seeded private
workspace/selfrepo with bundled note-taking tools and an extensible Cobra-based personal CLI
For AI agents working on this codebase: see CODEBASE.md for architecture and AGENTS.md for build/run instructions.
Goated works best on Linux (in a VM, a dedicated box, or a VPS). The maintainer recommends a DigitalOcean droplet or EC2 Instance with 4GB memory. You can run it on a Mac Mini if you want, but, like, buyer beware. Goated uses YOLO mode and it ain't in a docker container.
- Clone the repo:
git clone https://github.com/endgame-labs/goated.git
cd goated- Run the bootstrapper:
./bootstrap.shAfter bootstrap, you'll find a folder at workspace/self. That directory is its own Git repo and is meant to keep version-controlled history of the agent's tools, knowledge, prompts, and settings.
We recommend connecting workspace/self to a private remote on GitHub, GitLab, or another Git host so you can push it and preserve the agent's history independently of the main goated repo.
Example:
cd workspace/self
git remote add origin git@github.com:your-org/agent-self.git
git branch -M main
git push -u origin main- Go matching
go.mod— all binaries are compiled from this repo - tmux — hosts the persistent interactive runtime session (only needed for TUI runtimes)
- One agent runtime CLI —
claudefor Claude Code,codexfor Codex, orpifor Pi - Telegram Bot API — user-facing interface (bot token from @BotFather)
- bbolt — embedded key-value database (no external DB server)
| Binary | Size | Description |
|---|---|---|
goated |
11 MB | Control-plane CLI + daemon (daemon run, start, cron, bootstrap) |
goat |
11 MB | Agent-facing CLI (send_user_message, creds, cron, spawn-subagent) |
goated.db |
10-20 MB | bbolt embedded database (crons, subagent runs, metadata) |
Both binaries are statically-compiled Go with no runtime dependencies.
Memory at runtime: the daemon uses ~15-20 MB RSS. Subagents are separate runtime CLI processes. The goat CLI is exec'd per-call and exits immediately, so it adds no persistent memory cost.
For a detailed comparison of token usage, file sizes, and memory overhead vs. OpenClaw, see docs/PERFORMANCE.md.
┌──────────┐ ┌──────────────┐ prompt/paste ┌──────────────────────────┐
│ Telegram │ ──────> │ Gateway │ ───────────> │ Active Runtime │
│ or Slack │ │ (polling/ │ │ (headless or tmux) │
│ User │ <────── │ webhook) │ <────────── │ │
└──────────┘ └──────────────┘ exec └──────────────────────────┘
^ │ │ │
│ │ │ │ ./goat spawn-subagent
│ │ ./goat send_user_ │ │
│ │ message v v
└────────────────────┼──────────────────────────── ┌────────────────────┐
│ │ Subagent │
┌────v─────┐ │ (headless runtime) │
│ Cron │ ────────────────────────> │ │
│ Runner │ spawn └────────────────────┘
└──────────┘
Both the cron runner and the active runtime session can spawn subagents. The cron runner does it on a schedule; the runtime does it via ./goat spawn-subagent when it wants to delegate a task to a parallel worker. All subagents are tracked in bbolt.
Steady-state message flow:
- User sends a message via Slack or Telegram
- Gateway connector receives it (Socket Mode for Slack, long-polling/webhook for Telegram)
- Gateway posts a feedback indicator —
_thinking..._on Slack, typing animation on Telegram - Gateway checks active runtime session health (auto-restarts if unhealthy)
- The message is wrapped in a pydict envelope — a Python dict literal containing the message, source channel, chat ID, response command, and which formatting doc to use
TmuxBridge.SendAndWait()pastes the envelope into the tmux pane viatmux load-buffer+paste-bufferand presses Enter- The bridge polls the pane every 2s using content-change idle detection: the pane must be stable (unchanged across consecutive captures) AND contain the
❯prompt to count as idle — a single prompt check isn't enough because❯is often visible while Claude is actively working - The active runtime processes the request and pipes its markdown response into
./goat send_user_message --chat <id> - The
goatCLI converts markdown to the appropriate format (Slack mrkdwn or Telegram HTML) and posts it via the platform API - On Slack, the thinking indicator is deleted; if the runtime is still busy, a new one is posted and reaped when it goes idle
Key design choice: the runtime sends its own replies. The gateway doesn't scrape output from tmux — instead, the runtime is instructed through the workspace contract to pipe its response through the goat CLI. This makes the system stateless on the response path and avoids fragile scrollback parsing.
Headless runtimes use process-per-message execution: claude uses claude -p --resume, codex uses codex exec with codex exec resume for follow-up turns, and pi uses Pi's headless JSON/session modes. TUI runtimes (claude_tui, codex_tui) run inside tmux. Subagents and cron jobs always run headlessly. Each run is tracked in bbolt with PID and status. The cron runner skips a job if its previous run is still in-flight, preventing pile-ups from long-running tasks.
goated/
├── main.go # Entry point (builds ./goated)
├── build.sh # Builds both binaries
├── goated.json # Config (gitignored)
├── goated.db # bbolt database (gitignored)
│
├── cmd/
│ └── goated/ # Shared CLI (builds ./goated and ./workspace/goat)
│ └── cli/
│ ├── bootstrap.go # Interactive setup wizard
│ ├── creds.go # Credential management
│ ├── cron.go # Cron CRUD
│ ├── daemon.go # daemon run/start/stop/restart/status
│ ├── gateway.go # Run gateway standalone
│ ├── send_user_message.go # Agent → Telegram message push
│ ├── session.go # Active runtime session management (status/restart/send)
│ ├── spawn_subagent.go # Launch headless runtime worker
│ └── start.go # Foreground start (gateway + cron)
│
├── internal/
│ ├── app/config.go # Viper config loader + cred helpers
│ ├── agent/ # Provider-neutral runtime contracts
│ ├── claude/ # Claude headless runtime (claude -p --resume, hooks-based)
│ ├── claudetui/ # Claude TUI runtime implementations (tmux-based)
│ ├── pi/ # Pi headless runtime
│ ├── codextui/ # Codex TUI runtime implementations (tmux-based)
│ ├── cron/runner.go # Cron scheduler (1min tick, dedup, 1hr timeout)
│ ├── db/db.go # bbolt store (open-per-op, no held locks)
│ ├── gateway/
│ │ ├── service.go # Message routing, health checks, error handling
│ │ └── types.go # Handler/Responder/Connector interfaces
│ ├── subagent/run.go # Shared subagent execution (sync + background)
│ ├── telegram/connector.go # Telegram polling/webhook, offset persistence
│ └── util/ # Markdown→HTML, text sanitization
│
├── workspace/ # Agent working directory (GOAT_WORKSPACE_DIR)
│ ├── goat # Agent CLI binary (gitignored)
│ ├── GOATED.md # Shared runtime instructions
│ ├── CLAUDE.md # Claude compatibility shim
│ ├── GOATED_CLI_README.md # Agent CLI reference (committed)
│ ├── CRON.md # Instructions for cron-spawned agents (committed)
│ ├── creds/ # File-backed credentials (gitignored)
│ └── self/ # Agent's private repo (gitignored, see below)
│
├── docs/
│ ├── OPENCLAW_MIGRATION.md # Migration guide from OpenClaw
│ └── PERFORMANCE.md # Token, memory, and resource comparison vs OpenClaw
│
└── logs/ # All logs (gitignored)
├── goated_daemon.log
├── goated_daemon.pid
├── restarts.jsonl
├── cron/
│ ├── runs.jsonl
│ └── jobs/ # Per-run subagent logs
├── subagent/jobs/ # spawn-subagent logs
└── telegram/ # Chat logs
Everything committed in workspace/ is depersonalized and reusable — it's the platform contract that any agent can pick up. Personal state lives in workspace/self/, which is gitignored from this repo.
Committed (platform):
GOATED.md— shared runtime instructions and response contractCLAUDE.md— Claude compatibility shimGOATED_CLI_README.md— CLI referenceCRON.md— instructions for cron-spawned agents
Gitignored (personal, lives in workspace/self/):
IDENTITY.md— name, personality, voiceMEMORY.md— long-term memory (loaded every session)USER.md— info about the human they work withSOUL.md— values and beliefsAGENTS.md— workspace conventions and safety rulesTODO.md— agent's personal task listHEARTBEAT.md— heartbeat/pulse config and prompts- Projects, notes, drafts, tools, and anything else the agent creates
Bootstrap creates workspace/self/ as its own Git repo. We recommend connecting it to a private remote. This lets the agent:
- Version its own identity, memory, and project files
- Push/pull independently of the goated platform
- Survive workspace resets without losing accumulated context
For example:
cd workspace/self
git remote add origin git@github.com:your-org/agent-self.git
git branch -M main
git push -u origin mainThen add to workspace/self/AGENTS.md or similar:
Your self/ directory is a private git repo. Commit and push meaningful changes
to your identity, memory, and project files regularly.
- Go matching
go.mod(currently1.25.0) - tmux (only needed for TUI runtimes:
claude_tui,codex_tui) - A Telegram bot token (from @BotFather)
- One runtime CLI installed and authenticated:
- Claude Code (
claude) — used by bothclaudeandclaude_tuiruntimes - Codex (
codex) — used by bothcodexandcodex_tuiruntimes - Pi (
pi) — used by thepiruntime
- Claude Code (
Validate the machine before building:
scripts/setup_machine.sh doctorOn Ubuntu/Debian, you can install core packages with:
scripts/setup_machine.sh install-system
scripts/setup_machine.sh install-goinstall-system installs the baseline Ubuntu/Debian packages Goated expects
for day-to-day use, including tmux and cron/crontab.
git clone https://github.com/endgame-labs/goated.git
cd goated
bash build.shThis builds two binaries: ./goated and ./workspace/goat.
Run the interactive bootstrap:
./goated bootstrapThis creates a goated.json config file and writes secrets to workspace/creds/*.txt. You can also create goated.json manually from goated.json.example:
If you choose the pi runtime during bootstrap, Goated also initializes a
Goated-managed Pi session and warms it up from GOATED.md so the first real
message lands in an initialized session. Pi provider auth and custom provider
config remain Pi-native under ~/.pi/agent/ (auth.json, models.json) rather
than goated.json or workspace/creds/.
cp goated.json.example goated.json
# Edit goated.json with your settings, then set secrets:
./goated creds set GOAT_TELEGRAM_BOT_TOKEN your-bot-token
./goated creds set GOAT_ADMIN_CHAT_ID your-chat-idMigrating from .env: If you have an existing .env file, run ./goated migrate-config to split it into goated.json + creds files automatically.
Settings (goated.json):
| Key | Default | Description |
|---|---|---|
gateway |
telegram |
slack or telegram |
agent_runtime |
claude |
claude, codex, pi, claude_tui, or codex_tui |
default_timezone |
America/Los_Angeles |
Timezone for cron schedules |
workspace_dir |
workspace |
Agent working directory |
db_path |
./goated.db |
Path to bbolt database |
log_dir |
./logs |
Log directory |
telegram.mode |
polling |
polling or webhook |
telegram.webhook_addr |
:8080 |
Listen address for webhook mode |
telegram.webhook_path |
/telegram/webhook |
Webhook endpoint path |
telegram.allowed_chat_ids |
[] |
Required. List of chat IDs allowed to DM the bot (negative IDs for group chats) |
slack.channel_id |
"" |
Monitored Slack DM/channel ID |
Secrets (workspace/creds/*.txt, env vars override):
| Creds file / Env var | Description |
|---|---|
GOAT_TELEGRAM_BOT_TOKEN |
Telegram bot API token (required for Telegram) |
GOAT_ADMIN_CHAT_ID |
Chat ID for admin alerts when auto-recovery fails |
GOAT_SLACK_BOT_TOKEN |
Bot User OAuth Token (xoxb-...) |
GOAT_SLACK_APP_TOKEN |
App-Level Token (xapp-...) for Socket Mode |
Goated refuses to start with gateway=telegram until at least one chat ID is on the allowlist. This protects the bot from strangers who discover its handle. Bootstrap prompts for allowed IDs; you can also manage them later:
./goated channel allow <channel> <chat-id> # add an ID
./goated channel unallow <channel> <chat-id> # remove an ID
./goated channel allow-list <channel> # show current allowlistNegative IDs (groups) must be passed after -- so the shell doesn't parse them as flags:
./goated channel allow -- telegram -1001234567890To discover your personal chat ID, DM @userinfobot — it replies with your numeric ID. To discover a group's chat ID, temporarily add the bot to the group, send any command (for example /start@YourBot), and check the daemon log for the rejection line containing the negative chat_id.
Goated supports group chats. The bot only responds to group messages that address it:
- The message @-mentions the bot (
@YourBot what's up?), OR - The message is a reply to one of the bot's own messages, OR
- The message is a slash-command targeting the bot (
/ask@YourBot ..., or/askif privacy mode is off).
Everything else in a group is silently ignored — no "not authorized" reply — so the bot doesn't spam the group.
⚠️ Trust model. Allowlisting a group chat ID grants every current and future member of that group the ability to prompt the bot. Only allowlist groups whose members you trust as if they were you. The envelope does includeuser_idanduser_nameso the agent knows who is speaking (see PYDICT_FORMAT.md), but the bot will still honor any request the message makes. Remove the group from the allowlist the moment an untrusted person joins.
- Enable groups in @BotFather →
/mybots→ pick your bot → Bot Settings → Allow Groups? → Turn on. - (Strongly recommended) Disable group privacy so the bot reliably receives @-mentions:
- BotFather → same path → Group Privacy → Turn off.
- Telegram caches privacy mode per-membership, so you must remove and re-add the bot to the group after toggling it, or the bot will keep missing @-mentions. With privacy off, the bot sees every group message — Goated still filters to only respond to ones that address it, so the trust model doesn't change.
- Add the bot to the group: group header → Add Member → search for
@YourBot. - Discover the group's chat ID: in the group, send
/start@YourBot(slash-commands always reach the target bot, even with privacy on). The daemon log will print a rejection line:Copy the negativetelegram: rejected message from unauthorized chat_id=-5148442475 user_id=...chat_id. - Allowlist the group. Because the ID starts with
-, use--to stop shell flag parsing:./goated channel allow -- telegram -5148442475
- Restart the daemon.
@YourBotmentions and replies-to-bot in that group now reach the agent.
- @-mention to start a new thread:
@YourBot can you summarize the PR?. You must pickYourBotfrom Telegram's autocomplete dropdown — typing the name as plain text does not create a mention entity and the bot will not see it. - Reply to continue a thread: tap-and-hold the bot's message → Reply. No @-mention needed on follow-ups because Telegram delivers replies-to-bot even under privacy mode.
- Bot commands: anything like
/help@YourBotor/schedule@YourBot ...also works.
The prompt envelope delivered to the agent carries the group's chat_id, the sender's user_id / user_name / user_username, and chat_type. respond_with sends replies back into the same group automatically. The @YourBot mention is stripped from the message text before handing it to the agent.
# Foreground (dev)
./goated start
# Background daemon (prod)
./goated daemon runTo find your chat ID, message the bot and send /chatid.
/clear— start a fresh active-runtime session/chatid— show your chat ID/context— approximate context window usage/schedule <cron_expr> | <prompt>— store a scheduled job
The active runtime sends replies directly via ./goat send_user_message --chat <chat_id>.
./goated session status # Health, busy state, context estimate
./goated session restart # Restart the active session and preserve conversation when supported
./goated session restart --clear # Discard prior conversation and start fresh
./goated session send /context # Send a slash command or text to the active runtime
./goated session send "What are you working on?"session send pastes text directly into the active runtime tmux pane and presses Enter. Useful for sending runtime slash commands (/context, /clear) or ad-hoc prompts without going through the gateway.
./goated runtime status
./goated runtime switch claude # headless Claude Code
./goated runtime switch codex # headless Codex
./goated runtime switch pi # headless Pi
./goated runtime switch claude_tui # Claude Code in tmux
./goated runtime switch codex_tui # Codex in tmux
./goated runtime cleanup./goated daemon restart --reason "deployed new build"
./goated daemon stop
./goated daemon statusRestarts wait for in-flight messages to flush. Reasons are logged to logs/restarts.jsonl.
A cron watchdog ensures the daemon is always running. If the daemon dies for any reason, it will be restarted within 2 minutes:
# Install the watchdog:
(crontab -l 2>/dev/null; echo '*/2 * * * * /path/to/goated/scripts/watchdog.sh') | crontab -Logs to logs/watchdog.log.
./goated logs # last 50 lines of daemon signal (filtered, no Slack socket noise)
./goated logs -f # tail -f daemon signal (live)
./goated logs -n 200 # last 200 lines of daemon signal
./goated logs raw # last 100 lines unfiltered
./goated logs raw -f # tail -f unfiltered (everything)
./goated logs restarts # recent restart history
./goated logs cron # recent cron run log
./goated logs watchdog # watchdog log
./goated logs turns -n 20 # last 20 user/assistant turns from gateway message logs
./goated logs turns --days 5 # turns from the last 5 calendar days (inclusive)
./goated logs turns --since 2026-03-25 --until 2026-03-29
# turns within an explicit date range
./goated logs turns --chat D123 --days 3
# recent turns for a single chat onlylogs turns reads from logs/message_logs/daily and supports --chat, --days, --since, and --until. --days uses the configured default_timezone and cannot be combined with --since/--until.
All subcommands support -n to control line count. logs and logs raw also support -f for live tailing. Output goes to stdout, so you can pipe to grep, jq, etc.
The agent's tmux session runs inside workspace/, so all agent commands use ./goat (not workspace/goat).
# Send message to user
echo "Hello" | ./goat send_user_message --chat <chat_id>
# Credentials
./goat creds set API_KEY value
./goat creds get API_KEY
./goat creds list
# Cron jobs
./goat cron add --chat <chat_id> --schedule "0 8 * * *" --prompt "Morning summary"
./goat cron add --chat <chat_id> --schedule "0 8 * * *" --prompt-file /path/to/prompt.md
./goat cron list
./goat cron disable <id>
./goat cron enable <id>
./goat cron remove <id>
# Subagents
./goat spawn-subagent --prompt "Run a background task"Claude uses prompt-aware idle detection: the pane must be stable and show ❯. Codex uses a runtime-specific state classifier that distinguishes ready, generating, auth-blocked, and intervention-blocked screens instead of relying on a shared prompt glyph.
On message receipt, the daemon posts _thinking..._ to Slack. When the runtime sends its reply via goat send_user_message, the CLI deletes the thinking message. If the runtime is still busy, a new thinking indicator is posted and tracked. A TTL reaper acts as a safety net: soft deadline at 4 minutes, hard deadline at 20 minutes.
Every 5 messages, the gateway asks the active runtime for a context estimate. If the runtime reports a known usage above 80% and supports compaction, Goated sends /compact and queues any incoming messages until compaction finishes, then flushes the queue.
- Session health checks detect auth failures, API errors, and connectivity issues
- Auto-restarts the active runtime session up to 5 times (once per minute)
- If recovery fails, DMs the admin chat ID
- On startup, detects orphaned work from previous daemon and waits or recovers
- Telegram update offset is persisted so restarts don't replay old messages
- Cron jobs are deduped — a job won't fire again if its previous run is still in-flight
- Restart guardian:
goated daemon restartspawns a detached safety-net process that ensures the new daemon starts even if the restart command itself is interrupted - Watchdog cron: optional
scripts/watchdog.shchecks every 2 minutes that the daemon is alive and restarts it if not
See docs/OPENCLAW_MIGRATION.md for credential migration, cron migration, and example prompts.
MIT License. Copyright (c) 2025-2026 Kyle Wild and Endgame Labs, Inc. See LICENSE for details.
See SECURITY.md for private vulnerability reporting guidance.
See CONTRIBUTING.md for build and PR expectations.
