Skip to content

Repository files navigation

xeno

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.

Installation

1. Install xeno

brew install jiulongw/tap/xeno

2. Create an agent home directory

The home directory is where xeno stores the agent's configuration, memory, and skills. Choose a path and run:

xeno init ~/xeno-home

This 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.

3. Configure xeno

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 by init).
  • provider — AI provider to use: "claude" (default) or "codex". Can also be set via XENO_PROVIDER environment 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 to false to disable the built-in heartbeat task (default: true).

Obtaining a Telegram bot token

  1. Open Telegram and search for @BotFather.
  2. Send /newbot and follow the prompts to choose a name and username.
  3. BotFather will reply with an API token (e.g., 123456:ABC-DEF...). Copy this value into your config file.
  4. To get your Telegram user ID, send a message to your bot. If the service is not installed yet, run xeno serve manually so the gateway can receive Telegram messages. If your ID is not listed in telegram_allowed_users, the bot replies with your user ID so you can add it to the config.

Telegram bot settings recommendations

  • 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.

User whitelist

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.

4. Install the LaunchAgent

xeno install

This 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.

5. Upgrading

When you upgrade xeno to a new version:

brew upgrade xeno
xeno install

Always run xeno install after upgrading to restart the service with the new binary.

Upgrading from 0.3.x to 0.4.0

Template rename: CLAUDE.mdAGENTS.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:

  1. Check your agent home's CLAUDE.md for any agent-added content beyond the default template
  2. Copy those customizations somewhere safe (e.g., into TOOLS.md or a memory file)
  3. After upgrading, move the customizations into AGENTS.md (which is also auto-overwritten on startup, so persistent customizations should go in SOUL.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.

Uninstalling

To stop the service and remove the LaunchAgent:

xeno uninstall

Commands

  • serve — 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 socket
  • console — attach an interactive terminal chat console to a running serve process via Unix socket JSON-RPC (/hb triggers heartbeat immediately)
  • init <path> — create and initialize an agent home directory
  • install — install and start a macOS LaunchAgent for xeno serve
  • uninstall — 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.

Home directory

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 (with scripts/xeno-voice)
  • memory/ directory

When re-running initialization, CLAUDE.md and files under .claude/skills/ are refreshed from templates. Other existing files are preserved.

Topic channels

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 to sonnet model)
  • .claude/skills/run-cron-task/SKILL.md
  • An independent cron engine with xeno-cron MCP (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.

Telegram group chat behavior

  • 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

Development

Install dependencies

bun install

Build

bun run bundle

Build output:

  • bin/xeno.js

Run from source

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 uninstall

console requires a running serve process for the same --home. Socket path: <home>/.xeno/gateway.sock. RPC protocol reference: docs/gateway-json-rpc.md.

Telegram setup

Set TELEGRAM_BOT_TOKEN to enable Telegram service under serve.

You can also set telegram_bot_token in ~/.config/xeno/config.json.

  • serve enables Telegram automatically when a token is available
  • TELEGRAM_BOT_TOKEN overrides telegram_bot_token from config
  • Sending /start initializes bot commands for the chat (currently /compact and /stop)
  • Telegram /compact is forwarded as a raw slash command and bypasses platform-context wrapping
  • Telegram /stop aborts 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, fallback username) when passed to the agent

Cron and heartbeat

  • serve starts a cron engine and exposes cron task management to the agent via MCP server xeno-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-messenger with tool send_message so tasks can send proactive messages (default target: last known channel)
  • Cron task notify modes are auto and never
  • 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.json and 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 /hb or via JSON-RPC gateway.heartbeat

Config file

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 by XENO_PROVIDER env var.
  • heartbeat_interval_minutes (number): interval for built-in heartbeat task
  • heartbeat_enabled (boolean): enable/disable built-in heartbeat task (default true)
  • telegram_allowed_users (object or array): controls which Telegram users can interact with the bot and in which chats. See User whitelist below.

Executable overrides

  • 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.

Logging

Structured logs use pino. Set LOG_LEVEL to control verbosity:

LOG_LEVEL=debug bun run src/index.ts serve --home /tmp/xeno

Telegram inbound messages are logged at info with metadata including user/chat IDs, message type, detected slash command, attachment count, and short text/caption previews.

Known edge case

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.

GitHub Actions

  • CI workflow: .github/workflows/ci.yml
    • Triggers on pull_request and push to main
    • Runs:
      • bun install --frozen-lockfile
      • bunx prettier --check .
      • bun run check
      • bun run test
  • 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

About

No description, website, or topics provided.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages