How to install cli-bridge
npx skills add https://github.com/starchild-ai-agent/official-skills --skill cli-bridgeFull instructions (SKILL.md)
Source of truth, from starchild-ai-agent/official-skills.
name: cli-bridge version: 0.3.0 description: | Manage short-code bundles that authorize the local starchild CLI to talk to this agent, including the agent-shell local-exec channel.
Use when connecting or disconnecting the starchild CLI (e.g. mint a CLI bridge code, list my CLI bundles, revoke an old CLI session, or let the agent run shell commands on the user's own machine). delivery: script metadata: starchild: emoji: "π" skillKey: cli-bridge requires: bins: [python3] user-invocable: false author: starchild tags: [cli, akm, bridge, sc-chatroom, short-code]
cli-bridge β issue CLI bundles for the user's own starchild binary
This skill mints a fresh AKM key (scope=chat:bridge:cli) on the local
clawd, then registers it with sc-chatroom in exchange for a short opaque
code (sc_xxxxxxxx). The bundle handed to the user contains only that
short code β never the AKM secret, never the Fly machine id.
+----------------+ POST /agent/chat/stream +-----------------+
| starchild CLI | Bearer sc_xxxxxxxx | sc-chatroom |
| (user laptop) | --------------------------> | (gateway) |
+----------------+ +--------+--------+
|
resolves sc_β¦ β AKM + container_id
|
v
POST /chat/stream (Bearer sk_β¦
+ fly-force-instance-id)
+----------------------+
| user's own clawd |
| (Fly internal) |
+----------------------+
Why a short code instead of the raw AKM?
Earlier versions baked the AKM secret + Fly machine id into the bundle directly. That worked but had two downsides β the bundle leaked routing metadata when decoded, and any party that ever held the bundle held a permanent AKM secret. The short-code form fixes both:
- Bundle base64 decodes to
{d, c:"", k:"sc_β¦", s, exp, l}β no secret, no Fly machine id. cli-revoke <sc_β¦>kills just the short code; the underlying AKM stays alive (usecli-revoke --akm <prefix>to nuke that too).- sc-chatroom now holds the AKM secret in its DB. That's a deliberate trust shift β the AKM stays inside Fly's internal network instead of riding around on user laptops.
Scope boundary β read this first
cli-bridge covers exactly one path: the user's local CLI talking 1:1
to that user's own clawd. It is not a chatroom membership credential.
| Use case | Right credential | Wrong |
|---|---|---|
| Personal CLI β own clawd (this skill) | chat:bridge:cli AKM, fronted by sc_β¦ code | β |
| Join an sc-chatroom room | chat:thread:chatroom-{room_id} AKM via chatroom join | chat:bridge:cli AKM |
| Browse a public room as a guest | no credential needed | any AKM |
Prerequisites
Same as chatroom:
- AKM is installed in this clawd (
POST /api/keysworks on loopback) - AKM accepts
scope="chat:bridge:cli"and the/chat/streammiddleware allows arbitrarythread_idfor that scope (already shipped in clawd branchaladdin/feat/akm-chatroom) - sc-chatroom is on a build that includes
POST /cli-keys(migration 007+) FLY_MACHINE_ID(orCONTAINER_ID) env is setCHATROOM_PUBLIC_URLenv points at the sc-chatroom gateway (defaults tohttps://workroom.iamstarchild.com)CHATROOM_SERVER_URLenv points at the Fly-internal sc-chatroom (defaults tohttp://sc-chatroom.internal:8080)
Commands
cli-login β mint a new bundle
python3 skills/cli-bridge/scripts/cli_login.py --label "my laptop"
python3 skills/cli-bridge/scripts/cli_login.py --label "codex-vm" --ttl-days 14
Default TTL is 90 days; max is 365 days. Output is a one-liner the user
copies into starchild login. The bundle is opaque β sc-chatroom
resolves it on each call.
cli-list β show active bundles
python3 skills/cli-bridge/scripts/cli_list.py
python3 skills/cli-bridge/scripts/cli_list.py --include-revoked
Lists every CLI short code minted by this user on sc-chatroom. Columns: code, issued, expires, uses, label.
cli-revoke β kill a bundle
python3 skills/cli-bridge/scripts/cli_revoke.py sc_xxxxxxxx
python3 skills/cli-bridge/scripts/cli_revoke.py --akm sk_yyyyyy
Default: kills the short code in sc-chatroom; underlying AKM stays alive.
With --akm: also revokes the AKM on local clawd, taking out every
bundle backed by it.
Local shell via agent-shell (CLI β₯ v0.2.0)
A cli-login bundle minted with --enable-shell also authorizes the
agent to run shell commands on the user's own machine β for "is nginx
running on my laptop", "organize ~/Downloads", and the like. A plain bundle
is a chat bridge only and grants no shell access (see "Shell is off by
default" below). The user starts a small daemon:
starchild agent-shell # daemonizes; holds a WS open to your clawd
starchild agent-shell --foreground # attach to the terminal for debugging
starchild agent-shell-stop # stop the daemon
agent-shell refuses to start if the logged-in bundle wasn't granted shell
β it tells the user to get a --enable-shell bundle rather than connecting
a channel clawd would reject.
The daemon is single-instance (pidfile + flock) and macOS/Linux only. It self-updates at startup and periodically; downloaded binaries are verified against an embedded Ed25519 release key before swapping, so a hostile or MITM'd update server can't push arbitrary code to the user's machine.
How it works: the daemon dials wss://<chatroom>/ws/cli-shell with the
bundle's sc_β¦ code. sc-chatroom resolves the code and reverse-proxies
the WebSocket to the user's clawd machine β it accepts the laptop's
upgrade, opens its own upstream WS to clawd pinned with
fly-force-instance-id, and pumps bytes between the two (this is not
fly-replay: chatroom and clawd are different Fly apps, and cross-app
replay is rejected with 403). The AKM is injected server-side on the
upstream hop β it never reaches the laptop. clawd holds the connection in
its ShellHubService; the local_shell tool is then exposed to the LLM
only while a shell-capable laptop is connected, and pushes commands
down the socket.
Shell is off by default (capability gate)
cli-login does not grant shell unless --enable-shell is passed. The
AKM is the authoritative capability source: clawd reads it on the
/ws/cli-shell handshake and refuses every exec for a connection that
doesn't carry shell (#264). So a leaked plain bundle is a chat credential,
never local RCE.
- Grant shell:
cli_login.py --label β¦ --enable-shellβ AKMcapabilities: ["shell"], bundle carriesx: ["shell"]. - Upgrade an existing no-shell bundle: you can't flip it in place β mint a
new
--enable-shellbundle,starchild loginit, andcli-revokethe old one. Privilege escalation always goes through a fresh issuance.
What the agent knows up front (capability manifest)
On connect, the daemon sends a hello frame advertising:
- Platform β
os(darwin/linux),arch(arm64/amd64), and the activeshell. So the agent knows whether it's talking to BSD or GNU userland, which package manager to assume, etc. β no more guessingpsflags or hittingps: illegal option. - Policy summary β
mode(default-denywhen no allow rules exist, elseallowlist), the user'sallowedrules, explicitdenied_extrarules, and the always-onbuiltin_deniedlist.
clawd renders this into the agent's system prompt (only while connected), so the agent picks a permitted command β or tells the user plainly that the local policy forbids it β instead of probing blindly.
Session behavior
- Connection-level cwd. Each command's resulting working directory is
echoed back (via a trailing-
pwdsentinel stripped from stdout) and persisted for the next command, socdhas real meaning across calls within a session β without the cost/fragility of a full PTY. An explicit per-call cwd overrides it. - Output truncation. stdout/stderr are each capped at 200 lines (plus a
byte cap) so a
find /or log dump can't flood the LLM context. The full pre-truncation line count is reported (stdout_lines/stderr_lines), andtruncated: trueis set β the agent can say "showing first 200 of N lines" rather than truncating silently. - Heartbeat. The daemon pings every 45s to keep the idle WebSocket alive (Fly's edge cuts idle sockets at ~2.5min). Exec runs in a goroutine so a long command doesn't block heartbeats.
Local execution policy (the only auto-run guard)
The daemon runs headless (no TTY to prompt on), so every command is
gated by ~/.config/starchild/exec-policy.toml (parsed as a tiny
YAML allow:/deny: line format β no TOML dependency, despite the name).
Rules are substring matches by default; wrap a rule in / / for a
regex:
allow:
- "ls"
- "cat "
- "/^git (status|log|diff)/"
- "ps"
deny:
- "git push"
Decision order: built-in deny (always wins) β file deny β file
allow β default-deny. Two hard rules apply regardless of the file:
- A built-in deny list of interactive/TTY-blocking and destructive
commands is always refused:
vim/vi/nano/emacs,less/more/man,top/htop/btop,ssh/telnet,sudo/su/doas,tmux/screen,reboot/shutdown/halt, plus the shapesrm -rf,mkfs,dd if=,β¦ | sh,β¦ | bash,> /dev/sd*. - Default-deny: anything not matched by an
allowrule is denied. So with no policy file the policymodeisdefault-denyand nothing runs until the user opts commands in.
Limitations
- Unattended policy only. There is no interactive approval prompt; the policy file is the sole guard. A future version adds a web-approval popup.
- Synchronous commands only. No background jobs / progress polling yet.
- macOS/Linux only. The daemon refuses to run on Windows.
- Revocation:
cli-revoke <sc_β¦>kills the short code; the daemon's next reconnect then fails auth and the channel closes.
File transfer via agent-shell (CLI β₯ v0.3.0)
When the bundle is minted with --enable-files, the same agent-shell
daemon also serves file transfer between the user's machine and the
agent's workspace. Content streams diskβdisk and never passes through the
chat, so large/binary files (10MB+ PDFs, images, archives) work.
Three agent-facing tools + one user command:
request_upload(laptop_path)β agent pulls a file FROM the laptop intoworkspace/uploads/("take my ~/big.pdf and summarize it").write_local_file(src, dst)β agent sends a workspace file TO the laptop ("save workspace/output/report.pdf to my ~/Downloads").srcis a workspace path, not inline content.read_local_file(path)β read a small text file for the agent to see (config/log snippet). Large/binary files go throughrequest_upload.starchild push <file>β user proactively uploads a local file into the agent'sworkspace/uploads/; it's announced to the agent in its prompt.
python3 skills/cli-bridge/scripts/cli_login.py --label "laptop" --enable-files
# combine with shell if you want both:
python3 skills/cli-bridge/scripts/cli_login.py --label "laptop" --enable-shell --enable-files
files is an independent capability from shell β a bundle can have
either, both, or neither. Like shell, it's off by default and authoritative
on the AKM (clawd refuses transfer frames for a connection without it).
File path policy (laptop-side, layered)
Transfers are gated on the laptop by a path policy, strictest-first:
-
Built-in protected paths are ALWAYS refused (even under
--yolo):~/.ssh,~/.aws, shell rc (.zshrc/.bashrc/β¦),.config/starchild, launchd/systemd/cron,.git/hooks, browser cookie stores,.env, ssh keys. Writing those would be persistent RCE; reading them leaks creds. -
Dedicated transfer dir (
~/starchild-transfer, auto-created) β always allowed for read + write. The safe default workspace; prefer it. -
Outside that dir β denied unless the path matches a
read_allow/write_allowglob in~/.config/starchild/file-policy.toml, or the daemon was started with--yolo:starchild agent-shell --yolo # allow ANY path (built-in deny still applies)# ~/.config/starchild/file-policy.toml (YAML allow-globs) read_allow: - "~/Documents/*.md" write_allow: - "~/exports/*.csv"
Other guarantees: written files get mode 0644 (never executable); writes are atomic (temp file + rename, no half-written target); symlinks that escape the transfer dir are refused; per-transfer cap is 100 MiB, streamed in chunks so large files don't blow the WS frame limit.
Security note: a running
agent-shell(on a--enable-shellbundle) plus a permissive policy is effectively remote command execution on the user's machine, bounded by the AKM TTL, thesc_β¦code's validity, and the policy file. Defaults are conservative: shell is off unless explicitly granted, the policy is deny-all until commands are opted in, and the daemon's self-update verifies an Ed25519 signature before swapping binaries. Widen deliberately.
End-to-end smoke test
# 1. Inside agent chat:
@agent give me a cli key for my laptop
# β outputs `starchild login starchild_<base64>` (bundle has sc_β¦ code)
# 2. On laptop:
starchild login starchild_xxx
starchild whoami
starchild "hello, who are you?"
# β starchild sends Bearer sc_β¦ to sc-chatroom; sc-chatroom resolves
# β it to AKM + container_id and forwards to user's clawd
# 3. Revoke the short code from chat:
@agent revoke cli code sc_xxxxxxxx
# 4. Next CLI call should fail at the gateway:
starchild "hello?"
# β "gateway rejected (401) β code may be revoked; ask your agent for a fresh CLI bundle"
Pipe / shell composition (CLI β₯ v0.1.0)
Once paired, starchild is pipe-friendly. It reads stdin when no
positional prompt is given, writes the assistant reply to stdout, and
sends diagnostics to stderr β so it composes with any Unix tool.
# stdin β reply
echo "explain monads in 3 lines" | starchild
# reply β downstream
starchild "what is the OWASP top 10?" | pbcopy
# full three-stage pipe with streaming output
( echo "summarize this README:"; cat README.md ) | starchild --stream | tee summary.md
# code review pattern β concatenate context + question upstream
( echo "review this diff, flag risky changes:"; git diff ) | starchild
Gotcha: when you pass a positional prompt, stdin is ignored.
To send both context and an instruction, concatenate them upstream
with ( echo "<question>"; cat <file> ) rather than relying on
cat <file> | starchild "<question>" (which would silently drop the
file contents).
SOUL.md hint (recommended)
Add to your agent's SOUL.md so the LLM picks the right tool when the user asks for a CLI key:
## Issuing CLI bundles for the user's own bots/scripts
When the user asks "give me a cli key" / "create a starchild bundle" /
"let me talk to you from my terminal", run:
python3 skills/cli-bridge/scripts/cli_login.py --label "<inferred>"
This is a chat bridge only β it does NOT let you run commands on their
machine or touch their files. Two independent opt-in capabilities, each
granting local access β only add them when the user explicitly asks:
- `--enable-shell` β run commands ("run commands on my laptop", "use
agent-shell", "organize my Downloads"). Remote command execution.
- `--enable-files` β read/write files ("save this to my laptop", "read my
~/notes.md"). Reads/writes files on their machine.
python3 skills/cli-bridge/scripts/cli_login.py --label "<inferred>" --enable-shell
python3 skills/cli-bridge/scripts/cli_login.py --label "<inferred>" --enable-files
Treat both as granting access to their machine β never add either by
default or "to be helpful". If they later want a capability, mint a new
bundle with the flag and have them revoke the old one.
Default the label to something like "untitled-YYYY-MM-DD" if the user
doesn't suggest one. Show them the resulting bundle and tell them how
to revoke: `cli-list` to find the code, then `cli-revoke sc_β¦`.
After pairing, mention they can also pipe into the CLI from their
shell β e.g. `echo "..." | starchild`, `starchild "..." | pbcopy`,
or `( echo "review:"; git diff ) | starchild`. Stdout is the reply
(pipe-safe), stderr is diagnostics. Note the gotcha: passing a
positional prompt makes stdin get ignored, so context + question
should be concatenated upstream.
Related skills
More from starchild-ai-agent/official-skills and the wider catalog.
coinglass
Crypto derivatives data: funding rates, open interest, liquidations, long/short ratios, and whale tracking.
hyperliquid
|
wallet
|
coingecko
|
|
skill-creator
|