English | 한국어
A single daemon that lets you drive four local coding CLIs (Claude Code · Codex · Pi · Gemini) from Telegram, 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 (user messages, errors,
/help) is currently Korean-first; the bot itself works fine either way.
- 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.
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 doctor |
Check paths, agents, and plist status |
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 |
/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, launchd extraEnv
├── safety.json # persisted /safety on|off state
├── sessions.json # per-chat active-agent metadata
├── sessions/ # per-chat codex / pi / gemini directories
├── workspace/ # the agents' shared cwd (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. Past 3800 chars it rolls over to a new message.
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. - 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.
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.
All user-facing messages, errors, and /help are in Korean (English copy PRs welcome).
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). - macOS only.
Per-version changes live on GitHub Releases.
Publishing a new version:
# 1) bump the version (auto-creates commit + tag)
npm version patch # or minor / major
# 2) push commit + tag
git push --follow-tagsThen in the GitHub web UI: "Draft a new release" → pick the tag → Publish
release. .github/workflows/publish.yml runs automatically through
npm publish --access public. The workflow first verifies the release tag
matches the package.json version and fails without publishing on a mismatch.
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:
publish.yml) - In
.github/workflows/publish.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.