A personal AI agent runtime for macOS. Inspired by OpenClaw, xeno takes a simpler approach: it runs as a lightweight wrapper around Claude Code and focuses on doing one thing well — keeping a persistent, autonomous agent running on your Mac.
xeno requires a pre-authorized Claude Code installation with credentials stored in the macOS Keychain. It ships with pre-installed skills that use AppleScript to access essential macOS apps such as Mail, Calendar, Notes, Reminders, and more.
brew install jiulongw/tap/xenoThe home directory is where xeno stores the agent's configuration, memory, and skills. Choose a path and run:
xeno init ~/xeno-homeThis scaffolds the directory with default template files (prompt files, skill definitions, and Claude settings). Re-running initialization overwrites CLAUDE.md and files under .claude/skills/, while other existing files are preserved.
The previous step created a configuration file at ~/.config/xeno/config.json with default_home already pointing to your agent home directory. Edit it to add your Telegram bot token:
{
"default_home": "~/xeno-home",
"provider": "claude",
"telegram_bot_token": "YOUR_BOT_TOKEN",
"telegram_allowed_users": { "YOUR_USER_ID": ["*"] },
"heartbeat_interval_minutes": 30,
"heartbeat_enabled": true
}default_home— path to the agent home directory (set automatically byinit).provider— AI provider to use:"claude"(default) or"codex". Can also be set viaXENO_PROVIDERenvironment variable (takes precedence over config).telegram_bot_token— token for the Telegram bot that xeno uses as its chat interface.telegram_allowed_users— controls which Telegram users can interact with the bot and in which chats. Accepts an object mapping user IDs to allowed chat IDs (or"*"for all chats), or a legacy array of user IDs. See User whitelist for details.heartbeat_interval_minutes— interval in minutes between heartbeat runs (default:30).heartbeat_enabled— set tofalseto disable the built-in heartbeat task (default:true).
- Open Telegram and search for @BotFather.
- Send
/newbotand follow the prompts to choose a name and username. - BotFather will reply with an API token (e.g.,
123456:ABC-DEF...). Copy this value into your config file. - To get your Telegram user ID, send a message to your bot. If the service is not installed yet, run
xeno servemanually so the gateway can receive Telegram messages. If your ID is not listed intelegram_allowed_users, the bot replies with your user ID so you can add it to the config.
- Keep threaded mode off. xeno currently handles all Telegram threads in the same session.
- In group chats, disable topics. xeno currently handles all group topics in the same session.
telegram_allowed_users controls which Telegram users can interact with the bot. It supports two formats:
Object format (recommended) — map each user ID to a list of allowed chat IDs, or "*" for all chats:
{
"telegram_allowed_users": {
"123456789": ["*"],
"987654321": ["-1001234567890", "-1009876543210"]
}
}"*"grants access to all chats (private and group).- A list of chat IDs restricts the user to only those specific group chats. Private chats with the bot are always allowed for listed users regardless of the chat ID list.
Legacy array format — grants all listed users access to all chats (equivalent to "*"):
{
"telegram_allowed_users": ["123456789", "987654321"]
}User IDs can be strings or integers. When telegram_allowed_users is omitted or empty, all messages are rejected. If an unauthorized user sends a message, the bot replies with their user ID so it can be added to the config.
xeno installThis registers xeno as a macOS LaunchAgent (cc.novacore.xeno.gateway) so that it starts automatically when you log in. An active login session is required — xeno will not start after a reboot until you log in for the first time, since it runs as a LaunchAgent rather than a LaunchDaemon.
For reliable background execution, enable macOS power setting Prevent automatic sleeping when the display is off on the Mac running xeno. If the machine sleeps, responses can be delayed until the next wake cycle or heartbeat run.
Run xeno install again whenever you change the configuration file to pick up the new settings.
When you upgrade xeno to a new version:
brew upgrade xeno
xeno installAlways run xeno install after upgrading to restart the service with the new binary.
Template rename: CLAUDE.md → AGENTS.md
In 0.4.0, the agent workspace instructions moved from CLAUDE.md to AGENTS.md. The CLAUDE.md file is now a one-line redirect (@AGENTS.md). Both files are auto-managed — xeno init and xeno serve will create AGENTS.md and overwrite CLAUDE.md automatically.
What you need to do:
If your agent added custom rules or notes to CLAUDE.md at runtime (the template encourages self-modification), those edits will be lost when CLAUDE.md is overwritten to a redirect. Before upgrading:
- Check your agent home's
CLAUDE.mdfor any agent-added content beyond the default template - Copy those customizations somewhere safe (e.g., into
TOOLS.mdor a memory file) - After upgrading, move the customizations into
AGENTS.md(which is also auto-overwritten on startup, so persistent customizations should go inSOUL.md,TOOLS.md, or similar preserved files)
This applies to both the main home directory and any existing topic channel directories under <home>/channels/.
New config field: provider
The config file (~/.config/xeno/config.json) now supports a provider field ("claude" or "codex", default "claude"). Existing configs without this field continue to work — Claude is used by default. No action required unless you want to use the Codex provider.
To stop the service and remove the LaunchAgent:
xeno uninstallserve— start the gateway service (enables configured chat services such as Telegram), start cron scheduling (including heartbeat), and host a Unix socket JSON-RPC endpoint; exits with an error if another xeno process is already serving the same home socketconsole— attach an interactive terminal chat console to a runningserveprocess via Unix socket JSON-RPC (/hbtriggers heartbeat immediately)init <path>— create and initialize an agent home directoryinstall— install and start a macOS LaunchAgent forxeno serveuninstall— stop and remove the macOS LaunchAgent
--home <string> is optional. If omitted, xeno uses default_home from ~/.config/xeno/config.json. Resolved home paths are normalized to absolute paths.
The agent home directory contains the following files (scaffolded from templates on first creation):
CLAUDE.md,BOOTSTRAP.md,HEARTBEAT.md,IDENTITY.md,MEMORY.md,SOUL.md,TOOLS.md,USER.md.claude/settings.local.json.claude/skills/run-cron-task/SKILL.md.claude/skills/xeno-voice/SKILL.md(withscripts/xeno-voice)memory/directory
When re-running initialization, CLAUDE.md and files under .claude/skills/ are refreshed from templates. Other existing files are preserved.
When the bot receives messages from Telegram group chats (or any non-main channel), xeno creates an isolated topic channel with its own agent session, memory, and cron scheduler. Topic channels are stored under <home>/channels/<sanitized-key>/.
Each topic channel gets:
- Its own
CLAUDE.md(generated),USER.md,memory/directory .claude/settings.local.json(defaults tosonnetmodel).claude/skills/run-cron-task/SKILL.md- An independent cron engine with
xeno-cronMCP (persistent tasks in<channelDir>/cron-tasks.json) - Path restriction hooks that prevent access to the parent home's private files (
MEMORY.md,USER.md,HEARTBEAT.md,memory/) and sibling channel directories
System tasks (heartbeat, weekly-new-session) remain main-session-only.
- Non-mentioned messages in group chats are queued to a per-channel message queue
- When the bot is @mentioned, queued messages are flushed as context and included in the prompt
- In 2-member group chats, the bot auto-responds without requiring an @mention
- Replies are routed back to the originating chat
bun installbun run bundleBuild output:
bin/xeno.js
bun run src/index.ts serve --home /tmp/xeno
bun run src/index.ts console --home /tmp/xeno
bun run src/index.ts install --home /tmp/xeno
bun run src/index.ts uninstallconsole requires a running serve process for the same --home.
Socket path: <home>/.xeno/gateway.sock.
RPC protocol reference: docs/gateway-json-rpc.md.
Set TELEGRAM_BOT_TOKEN to enable Telegram service under serve.
You can also set telegram_bot_token in ~/.config/xeno/config.json.
serveenables Telegram automatically when a token is availableTELEGRAM_BOT_TOKENoverridestelegram_bot_tokenfrom config- Sending
/startinitializes bot commands for the chat (currently/compactand/stop) - Telegram
/compactis forwarded as a raw slash command and bypasses platform-context wrapping - Telegram
/stopaborts the active query (if any) and sends a follow-up prompt asking what to do next - Non-command Telegram messages include sender name context (
first_name, fallbackusername) when passed to the agent
servestarts a cron engine and exposes cron task management to the agent via MCP serverxeno-cron- Available cron MCP tools:
create_cron_task,list_cron_tasks,update_cron_task,delete_cron_task - Cron and heartbeat runs execute through the gateway agent runtime (cron runs are prefixed as
/run-cron-task task_id:<task_id> now:<iso_timestamp>, heartbeat runs as/heartbeat now:<iso_timestamp>) - During cron execution, xeno injects MCP server
xeno-messengerwith toolsend_messageso tasks can send proactive messages (default target: last known channel) - Cron task
notifymodes areautoandnever - Cron task model selection is not user-configurable; runs use the internal default model
- Cron engine result callbacks are currently not auto-broadcast to chat channels
- Main session persistent cron tasks are stored at
<home>/cron-tasks.json - Topic channel cron tasks are stored at
<channelDir>/cron-tasks.jsonand execute on the channel's own session - Built-in heartbeat task:
- Reads
HEARTBEAT.md - Runs every 30 minutes by default
- Is runtime-only (not persisted in
cron-tasks.json) - Main-session-only (topic channels do not get heartbeat or weekly-new-session tasks)
- Can be triggered manually from console with
/hbor via JSON-RPCgateway.heartbeat
- Reads
Path: ~/.config/xeno/config.json
Example:
{
"default_home": "/tmp/xeno",
"provider": "claude",
"telegram_bot_token": "123456:abcdef",
"telegram_allowed_users": {
"123456789": ["*"],
"987654321": ["-1001234567890"]
},
"heartbeat_interval_minutes": 30,
"heartbeat_enabled": true
}Optional config keys:
provider("claude"|"codex"): AI provider (default"claude"). Overridden byXENO_PROVIDERenv var.heartbeat_interval_minutes(number): interval for built-in heartbeat taskheartbeat_enabled(boolean): enable/disable built-in heartbeat task (defaulttrue)telegram_allowed_users(object or array): controls which Telegram users can interact with the bot and in which chats. See User whitelist below.
PATH_TO_CLAUDE_CODE_EXECUTABLE— override the Claude CLI path used by the Claude provider. If unset, the Claude Agent SDK default executable resolution is used.PATH_TO_CODEX_EXECUTABLE— override the Codex CLI path used by the Codex provider.
Structured logs use pino. Set LOG_LEVEL to control verbosity:
LOG_LEVEL=debug bun run src/index.ts serve --home /tmp/xenoTelegram inbound messages are logged at info with metadata including user/chat IDs, message type, detected slash command, attachment count, and short text/caption previews.
In headless SSH sessions, first-time authentication can fail if the macOS keychain is locked. If needed, run security unlock-keychain and retry.
Claude authentication failures can sometimes appear in output (for example, Not logged in · Please run /login) while the SDK result stats still report result=success. This is an upstream edge case, so xeno currently displays both as-is across modes.
- CI workflow:
.github/workflows/ci.yml- Triggers on
pull_requestandpushtomain - Runs:
bun install --frozen-lockfilebunx prettier --check .bun run checkbun run test
- Triggers on
- Release workflow:
.github/workflows/release.yml- Triggers on pushed tags matching
v* - Runs
bun run bundle - Copies required Claude Agent SDK runtime files into
bin/(cli.js,*.wasm,vendor/) - Packages build output as
dist/xeno-<tag>.tar.gz - Generates checksum file
dist/xeno-<tag>.tar.gz.sha256 - Uploads both files to the GitHub Release for the tag
- Triggers on pushed tags matching