Control Claude Code, Codex, Pi & Gemini running on your Mac — from your phone, via Telegram.
English | 한국어
Coding agents are great at long-running tasks — until they finish, or get stuck waiting for a y/n confirmation, while you're away from your desk. cliclaw removes the tether to your chair: kick off a task before you leave, watch progress stream into Telegram, approve or deny dangerous commands from your phone, and send follow-up instructions from wherever you are.
Under the hood it's a single daemon on your Mac that turns a Telegram chat into a remote control for four local coding CLIs (Claude Code · Codex · Pi · Gemini), switching between them per chat.
It keeps an independent per-agent session for every chat, and ships a confirm gate for dangerous commands, response streaming, image-attachment handling, and auto-detection of corporate TLS interceptors (Zscaler, etc.).
Install only the CLIs you want — missing agents are automatically dropped from the active list.
The chat UX (messages,
/help, confirm buttons) is localized — English by default on new installs, Korean available via"locale": "ko"in config.json (existing pre-1.0 configs keep Korean automatically).
- macOS (Apple Silicon / Intel)
- Bun 1.x —
curl -fsSL https://bun.sh/install | bash - At least one of the coding CLIs installed and logged in (the bot merely spawns authenticated child processes)
- Claude Code:
npm install -g @anthropic-ai/claude-code - Codex:
npm install -g @openai/codex - Pi:
npm install -g @earendil-works/pi-coding-agent - Gemini:
npm install -g @google/gemini-cli
- Claude Code:
# Bun (recommended)
bun add -g @younggichoi/cliclaw
# or npm
npm install -g @younggichoi/cliclawThe package publishes under a scoped name (
@younggichoi/cliclaw), but the installed command is simplycliclaw.
cliclaw initFive steps, guided:
Welcome to cliclaw setup.
Bot language for Telegram chat UX — en/ko [en]: en
Step 1/5 — Telegram bot token
Get one from @BotFather (/newbot) on Telegram.
Bot token: 1234:ABC...
✓ @yourbotname (id=...) verified
Step 2/5 — Detect installed coding agents
✓ claude 2.1.139 (Claude Code) @ ~/.nvm/.../bin/claude
✓ codex 1.0.0 @ /opt/homebrew/bin/codex
✓ pi unknown @ /opt/homebrew/bin/pi
✓ gemini 0.42.0 @ ~/.nvm/.../bin/gemini
Default agent? [claude] (claude/codex/pi/gemini): claude
Step 3/5 — Authorize your Telegram account
Open Telegram and send any message to @yourbotname now.
Waiting up to 5 minutes... press Ctrl-C to abort.
✓ Received from user_id=123456789
Authorize this Telegram user? [Y/n] y
Step 4/5 — Corporate TLS interceptor (optional)
$NODE_EXTRA_CA_CERTS: /path/to/Zscaler.pem
Apply this CA certificate to the bot's LaunchAgent environment? [Y/n] y
Step 5/5 — Auto-start at login (launchd)
Install LaunchAgent so the bot starts automatically on login? [Y/n] y
✓ Installed ~/Library/LaunchAgents/com.alice.cliclaw.plist
✓ Bot started.
All set.
Logs: tail -f ~/.cliclaw/logs/bot.log
Test: send /status in Telegram.
That's it. The bot restarts automatically after screen lock or a reboot.
| Command | What it does |
|---|---|
cliclaw init |
Interactive setup (token, agent detection, telegram-id capture, CA, launchd) |
cliclaw start |
Run the bot in the foreground (for testing) |
cliclaw install-launchd |
Install the LaunchAgent (auto-applies launchd.extraEnv from config.json) |
cliclaw uninstall-launchd |
Remove the LaunchAgent |
cliclaw upgrade (alias: update) |
Pull the latest npm release and reinstall the LaunchAgent |
cliclaw version (--version, -v) |
Print the installed cliclaw version |
cliclaw logs [--audit|--err] |
Tail a log file (default: bot.log; --audit: audit.jsonl; --err: bot.err) |
cliclaw doctor |
Live checks: token, launchd, agents, TLS CA |
cliclaw help |
Help |
| Command | What it does |
|---|---|
/claude /codex /pi /gemini |
Switch this chat's active agent (uninstalled agents are hidden) |
/status |
Per-agent session status + any job in progress |
/health |
Bot system status (uptime, memory, log sizes, chat/job counts) |
/stop |
Cancel this chat's running job (SIGTERM → SIGKILL after 5s) |
/reset |
Discard only the current active agent's session |
/reset all |
Discard every agent session in this chat |
/safety |
Show safety-mode status — toggle with /safety on / /safety off |
/model |
Show/set this chat's per-agent model override — /model <name> to set, /model default to clear |
/plan |
Claude-only plan-mode toggle — /plan on spawns Claude with --permission-mode plan (propose, don't edit); /plan off reverts |
//<cmd> |
Native slash-command passthrough — strips one leading / and sends <cmd> verbatim as the prompt (e.g. //compact); whether the underlying CLI honors it in headless mode is up to that CLI |
/start /help |
Help |
| Any other text / photo | Sent as a prompt to the active agent (photos are downloaded and their path is prepended to the prompt) |
Switching agents keeps the old session intact — come back and continue. Sending
a new prompt to a chat with a running job is rejected (use /stop or wait).
All state is isolated under ~/.cliclaw/:
~/.cliclaw/
├── config.json # mode 600; token, allowlist, locale, launchd extraEnv
├── bot.pid # single-instance lock (holder PID)
├── safety.json # persisted /safety on|off state
├── sessions.json # per-chat active-agent metadata, model overrides, plan mode
├── sessions/ # per-chat claude / codex / pi / gemini state
│ └── claude/<chatId>/ # Claude's per-chat cwd (+ its own .claude/settings.json)
├── workspace/ # shared cwd for codex / pi (sandbox)
│ ├── .claude/settings.json # dangerous-command hook + deny rules when safety is ON
│ └── uploads/<chatId>/ # Telegram photo downloads
├── logs/
│ ├── bot.log # token auto-masking applied
│ ├── bot.err # launchd stderr
│ └── audit.jsonl # audit log (decisions, safety state)
└── .sock/ # confirm-gate IPC
The state directory can be moved with the CLICLAW_HOME env var:
CLICLAW_HOME=~/my-bot cliclaw initNo absolute paths in config.json. At startup, agents are discovered in three passes:
~/.local/bin,~/.claude/local,/usr/local/bin,/opt/homebrew/binbin/<cmd>of the newest node under$NVM_DIRor~/.nvmcommand -v <cmd>in a login shell (PATH with.zshrcloaded)
Undetected agents are gracefully skipped. The bot runs fine with any subset of the four.
ON (default):
- Dangerous Bash commands (
rm -rf,git push --force, DROP,kubectl delete, AWSdelete-*,sudo,curl|sh, ssh prd-*, …) are re-confirmed via a Telegram inline keyboard[✅ Allow] [❌ Deny]— no response means auto-deny. - Claude's Read tool denies sensitive files:
~/.ssh/**,~/.aws/**,~/.gnupg/**,~/.netrc,~/.npmrc,**/.env*,**/*.pem,**/id_rsa*,**/id_ed25519*,./secrets/**. - Add your own regexes via
confirmGate.extraPatterns.
OFF: if your environment already has an external guard (pre-bash-guard,
EDR, …) and the bot's confirm prompts feel redundant, turn it off with one line
in Telegram. Deny rules are disabled together. Every IPC request is still
recorded in logs/audit.jsonl as decision: allow, reason: safety_off.
The state persists in $CLICLAW_HOME/safety.json across restarts.
Consumes text_delta from --include-partial-messages and live-updates via
editMessageText, debounced at 1.5s. Rollover to a new message is budgeted
against the converted HTML length (not raw text), unfinished code fences render
correctly mid-stream, and a segment that permanently fails to send falls back
to plain text instead of silently disappearing.
Telegram photos/image documents are downloaded to
workspace/uploads/<chatId>/<msgId>.<ext> and the path is prepended to the prompt.
- Claude: runs with
--permission-mode bypassPermissions— dangerous Bash is caught by the confirm gate, and sensitive files by safety-mode deny rules./plan onswitches to--permission-mode plan(propose, don't edit). - Codex:
sandbox=workspace-writeby default. Never usedanger-full-access. - Pi: default mode.
- Gemini:
approvalMode=auto_editby default (edits auto-approved, destructive commands prompt). More autonomousyoloor more conservativedefault/planavailable. Note: Gemini does not integrate with cliclaw's confirm gate — its ownapprovalModeis the only shell-level defense (see SECURITY.md).
Where Zscaler / Forticlient / Cisco Umbrella intercepts HTTPS, Node cannot
trust Telegram's certificate and the bot cannot run. Step 4 of cliclaw init
auto-detects $NODE_EXTRA_CA_CERTS or launchctl getenv NODE_EXTRA_CA_CERTS,
asks you, and persists it into launchd.extraEnv in config.json. Every later
install-launchd bakes it into the plist automatically.
Everything written to logs/bot.log / bot.err is pre-redacted:
- Telegram bot tokens (
\d{8,}:[A-Za-z0-9_-]{30,}) - npm tokens (
npm_…) - GitHub PATs (
gh[pousr]_…) - exact matches of the live
config.token
Defends against Time Machine backups, EDR, and shoulder surfing alike.
Every user-facing string — messages, errors, /help, and the confirm-gate
buttons (✅ Allow / ❌ Deny) — comes from a typed en/ko message table.
config.locale selects the language: new installs default to English, and
configs written before 1.0 keep Korean automatically.
- Single-instance lock (
bot.pid) — a secondcliclaw startcan't race Telegram polling or hijack the confirm-gate socket. - No crash loops — a broken config makes the daemon go dormant and retry every 60s instead of hot-looping under launchd.
- Graceful shutdown — in-flight jobs are cancelled and their chats notified before exit.
- Process-group kill —
/stopand timeouts also terminate the agent's grandchildren (MCP servers, git, …), not just the CLI itself. - Retention sweeps — stale
uploads/andsessions/directories are cleaned up automatically (sessionRetentionDays, default 30).
Answering "Yes" at step 5 of cliclaw init:
- Creates
~/Library/LaunchAgents/com.<username>.cliclaw.plist(corporate CA baked in) - Loads and starts it immediately via
launchctl bootstrap gui/$UID <plist> - Auto-restarts on login / boot / crash from then on
- stdout →
~/.cliclaw/logs/bot.log, stderr →bot.err
Manual management:
# stop (auto-restarts on next login)
launchctl kill SIGTERM gui/$UID/com.<username>.cliclaw
# fully disable (no auto-restart either)
cliclaw uninstall-launchd
# re-enable (auto-applies launchd.extraEnv from config.json)
cliclaw install-launchd- The bot token = a remote shell into every installed agent. If it leaks,
/revokeat BotFather immediately. - An empty
allowedUserIdsrejects all messages (fail-closed). - Keep
config.jsonat mode600(init sets it automatically). - Never set
confirmGate.enabled: falseor switch the codex sandbox todanger-full-access. - Opting Gemini's
approvalModeintoyoloauto-approves every tool — use it with full understanding. - When in doubt,
/safety onre-activates the deny rules instantly.
To install without the cliclaw init flow:
git clone https://github.com/choiyounggi/cliclaw.git
cd cliclaw
bun install
mkdir -p ~/.cliclaw
cp config.example.json ~/.cliclaw/config.json
chmod 600 ~/.cliclaw/config.json
# fill token and allowedUserIds in config.json, then
bun run bot.tsbun run test- Dangerous patterns are regex-based — 100% classification is impossible; you own the policy.
- No body-text streaming for Codex / Pi / Gemini (no structured events, or not integrated).
- Gemini's dangerous commands rely solely on its own
approvalMode(bash-confirm IPC not integrated). - The hook holds the IPC while waiting for the user's decision.
- No voice/file attachments (photos only).
- Concurrent messages in the same chat are rejected (
/stopor wait). - 1:1 chats only — group/supergroup/channel chats are politely rejected.
- macOS only.
Per-version changes live on GitHub Releases.
Releases use changesets:
- In the PR with a user-visible change, describe it (patch / minor / major):
bunx changeset
- Merge that PR.
.github/workflows/release.ymlopens (or updates) a "chore: version packages" PR that bumpspackage.jsonand writesCHANGELOG.md. - Merge the version PR. The same workflow runs
changeset publish(npm publish), pushes thevX.Y.Ztag, and creates the GitHub Release. The publish runs only after type check and tests pass in that same job.
Do not bump the version by hand (npm version): any push to main whose
package.json version is not on npm yet gets published right away.
.github/workflows/publish.yml stays only as a fallback for a Release created
by hand in the GitHub UI.
One-time prerequisite (version PR): repo Settings → Actions → General → enable "Allow GitHub Actions to create and approve pull requests".
One-time prerequisite: repo Settings → Secrets and variables → Actions → register NPM_TOKEN with an npm token capable of 2FA bypass.
- https://www.npmjs.com/settings/younggichoi/tokens/new
- Issue a Granular Access Token or a Classic Automation token (with 2FA bypass)
- Add the
npm_…token as the GitHub Actions secretNPM_TOKEN
Hardening option: switch to npm Trusted Publishing (OIDC) and no token is needed at all.
- At https://www.npmjs.com/package/@younggichoi/cliclaw/access, add Trusted Publisher → GitHub Actions (workflow filename:
release.yml) - In
.github/workflows/release.yml, addid-token: writetopermissionsand removeNODE_AUTH_TOKEN; inpublish.yml, addpermissions: id-token: write, removeNODE_AUTH_TOKEN, add the--provenanceflag - Delete the old NPM_TOKEN secret
- Code contributions: CONTRIBUTING.md
- Dev flow & directory structure: DEVELOPMENT.md
- Security policy, threat model, vulnerability reports: SECURITY.md
MIT. See LICENSE.