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)
Bun1.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 add -g @younggichoi/cliclaw
npm install -g @younggichoi/cliclaw
The package publishes under a scoped name (
@younggichoi/cliclaw
), but the installed command is simply.cliclaw
cliclaw init
Five 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 init
No absolute paths in config.json
. At startup, agents are discovered in three passes:
~/.local/bin
,~/.claude/local
,/usr/local/bin
,/opt/homebrew/bin
bin/<cmd>
of the newest node under$NVM_DIR
or~/.nvm
command -v <cmd>
in a login shell (PATH with.zshrc
loaded)
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-write
by default. Never usedanger-full-access
.Pi: default mode.** Gemini**:approvalMode=auto_edit
by default (edits auto-approved, destructive commands prompt). More autonomousyolo
or more conservativedefault
/plan
available.
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:
launchctl kill SIGTERM gui/$UID/com.<username>.cliclaw
cliclaw uninstall-launchd
cliclaw install-launchd
The bot token = a remote shell into every installed agent. If it leaks,/revoke
at BotFather immediately.- An empty
allowedUserIds
rejects all messages (fail-closed). - Keep
config.json
at mode600
(init sets it automatically). - Never set
confirmGate.enabled: false
or switch the codex sandbox todanger-full-access
. - Opting Gemini's
approvalMode
intoyolo
auto-approves every tool β use it with full understanding. - When in doubt,
/safety on
re-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
bun run bot.ts
bun 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 (
/stop
or wait). - macOS only.
Per-version changes live on GitHub Releases.
Publishing a new version:
npm version patch # or minor / major
git push --follow-tags
Then 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--provenance
flag - 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
.