English | 简体中文
A friendly CLI shell for sing-box — proxied from boot, no GUI required, no user login required, all system traffic through a TUN proxy.
- Proxy from boot: systemd brings up the TUN interface before any user logs in. SSH, apt, every user process — all routed through the proxy.
- Zero GUI dependency: pure CLI. Runs on desktops, servers, headless boxes, containers.
- One-shot share-link import: supports
vless://vmess://trojan://ss://hysteria2://tuic:// - Hot node switch: applied instantly via sing-box's Clash API, no service restart.
- Live route mode switch:
rule/global/directwith one command. - Auto ruleset update: systemd timer pulls
.srsrulesets at a configurable cadence. - Bilingual: English (default) and Simplified Chinese; pick at install time, switch any time with
sc lang en|zh.
- Linux with systemd or OpenRC — tested on Debian, Ubuntu, Fedora, RHEL/CentOS/Rocky/Alma, Arch/Manjaro, openSUSE, Alpine
- amd64 (x86_64) or arm64 (aarch64)
- Python 3.6+ (preinstalled on most distros)
- root (one-time sudoers setup, password-less afterwards)
One line:
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/Alan-IFT/singbox-cli/main/install.sh)"The installer will:
- Prompt you to choose CLI language — English (default) or Simplified Chinese
- Download the sing-box binary from GitHub Releases and install it to
/usr/local/bin/sing-box - Install the
scCLI to/usr/local/bin/ - Create the service unit (systemd service + ruleset auto-update timer, or OpenRC init script on Alpine)
- Configure password-less sudo (scoped to the
sccommand only) - Download
.srsrulesets - Start sing-box and enable boot autostart
The language defaults to whatever your
$LANGenv var suggests (Chinese locale →zh, otherwiseen). Just hit Enter at the prompt to accept, or pick1/2.
Re-run the same one-liner. install.sh is idempotent: it overwrites the sc binary and systemd units but leaves nodes.json / settings.json untouched, so your nodes are preserved.
Inspect first, then run (recommended for the cautious):
curl -fsSL https://raw.githubusercontent.com/Alan-IFT/singbox-cli/main/install.sh -o install.sh
less install.sh
sudo bash install.shgit clone (for development):
git clone https://github.com/Alan-IFT/singbox-cli.git
cd singbox-cli
sudo ./install.shsc add 'vless://uuid@host:443?security=reality&pbk=...&fp=chrome&flow=xtls-rprx-vision#LosAngeles-US'
⚠️ Share links contain?&#and other shell-special characters. Wrap the link in single quotes.
sc ls # list all nodes
sc use 1 # by index
sc use US # by name fragmentSwitching is applied instantly via the Clash API — no service restart.
sc mode rule # rule-based routing (default)
sc mode global # everything via proxy
sc mode direct # everything directsc on # start + enable on boot
sc off # stop + disable on boot
sc status # service status, TUN interface, current node, egress IP
sc doctor # one-pass read-only health report (see below)
sc log -f # follow logs in real timesc doctorOne pass, one screen, seven facts — printed in causal order, so every cause appears above the effects it can produce:
| # | Section | What it reports |
|---|---|---|
| 1 | sing-box binary | the resolved path of the binary and its version |
| 2 | Rule-sets | one row per .srs: usable / missing / not a rule-set file / too small / unreadable, plus the byte count from that same read |
| 3 | Configuration | whether config.json exists, and what sing-box check says about it |
| 4 | Service | running now, and registered to start at boot — two separate facts |
| 5 | TUN interface | whether sb-tun exists, and its addresses |
| 6 | Clash API | the port recorded in settings.json, and whether it answers |
| 7 | Egress IP | the observed public address (queried even when the service is down) |
Every row is marked [OK], [PROBLEM] or [UNKNOWN] ([正常] / [异常] / [未知] under sc lang zh), so sc doctor | grep '^\[PROBLEM\]' lists exactly what is wrong. [UNKNOWN] means the check could not run at all — a missing tool, a permission denial — never "the thing being checked is broken". One failing check never ends the run: all seven sections are always printed.
sc doctor changes nothing. It writes no config, downloads nothing, and never starts, stops, restarts, enables or repairs anything. Unlike every other subcommand it does not even create /etc/sing-box or persist a Clash API port on first run — on a broken or fresh machine the emptiness of those paths is often the diagnosis, and a diagnostic must not destroy the evidence it was run to collect. It is safe to run repeatedly, concurrently, and as the very first thing after a failure.
Exit status:
| Exit | Meaning |
|---|---|
0 |
every section OK |
1 |
at least one [PROBLEM] — any section: a missing binary, an unusable rule-set, a failed config check, a stopped or non-autostarting service, a missing TUN device, an unanswered Clash API port, a failed egress query |
2 |
no [PROBLEM], but at least one [UNKNOWN] — a check could not run: no sing-box binary to check the config with, no init system detected, ip missing, or no Clash API port recorded in settings.json |
sc update-rules # update once now
sc update-rules --mirror <base-url> # force a specific mirror (repeatable)
sc update-interval daily # update every day
sc update-interval weekly # update every week (default)
sc update-interval 'Mon *-*-* 04:00:00' # every Monday at 04:00
sc update-interval show # show current cadence + next runsc update-rules tries several mirrors in order (jsDelivr → testingcf → ghfast → raw.githubusercontent) and validates every download before installing it, so a truncated body or an HTML error page is never written to /etc/sing-box/rules/. Progress is shown while downloading on a terminal; redirected output keeps one completion line per ruleset.
--mirror replaces the built-in mirror list (it does not fall back to it), is repeatable, and one value may hold several whitespace-separated URLs. The SB_RULES_BASE="<url> [url...]" environment variable does the same, but only when sc already runs as root (the systemd timer, a root shell) — from a normal shell sc re-execs itself through sudo, whose default env_reset drops the variable. Prefer --mirror.
A ruleset that cannot be downloaded is no longer fatal. The generated config drops that ruleset and every routing rule referencing it, warns which ones are unusable and why, and the service still starts — you lose routing granularity, not connectivity. Run sc update-rules (or sc reload once the files are in place) and the full rules come back automatically.
sc lang en # English
sc lang zh # 简体中文The setting is persisted in /etc/sing-box/settings.json and applies to all subsequent sc output (errors, status, help).
sc helpboot
└─ systemd starts sing-box (root)
├─ reads /etc/sing-box/config.json
├─ creates the sb-tun interface (172.19.0.1/30)
├─ connects to nodes directly (no user login required)
└─ loads local .srs rulesets
↓
all system traffic through the proxy (incl. SSH pre-login, GDM login screen)
User runs the sc CLI:
└─ edits /etc/sing-box/nodes.json or settings.json
└─ regenerates config.json
└─ Clash API tells sing-box to apply changes (no restart)
| Purpose | Path |
|---|---|
| sing-box binary | /usr/local/bin/sing-box |
| sc CLI | /usr/local/bin/sc |
| sing-box config (auto-generated) | /etc/sing-box/config.json (mode 600) |
| Node list (with credentials) | /etc/sing-box/nodes.json (mode 600) |
| Settings | /etc/sing-box/settings.json |
| Your own config overrides (optional, yours) | /etc/sing-box/override.json |
Record of what sc last generated (internal) |
/etc/sing-box/.config.sha256 |
| Rulesets | /etc/sing-box/rules/*.srs |
| systemd service | /etc/systemd/system/sing-box.service (systemd only) |
| Auto-update timer | /etc/systemd/system/sing-box-rules-update.timer (systemd only) |
| Auto-update cadence override | /etc/systemd/system/sing-box-rules-update.timer.d/override.conf (systemd only) |
| OpenRC service | /etc/init.d/sing-box (OpenRC/Alpine only) |
| Periodic update scripts | /etc/periodic/{daily,weekly,monthly}/singbox-update-rules (OpenRC/Alpine only) |
| Password-less sudo | /etc/sudoers.d/sc |
| Uninstall script | /usr/local/lib/singbox-cli/uninstall.sh |
| Logs | journalctl -u sing-box or sc log (systemd); sc log reads /var/log/sing-box/ on OpenRC |
config.json is generated: sc reload, sc add and sc rm rewrite it from scratch every time, and sc use and sc update-rules may do so as well, so anything you hand-edit there is discarded without a word. Put your changes in /etc/sing-box/override.json instead. sc never creates, writes or deletes that file, and applies it last — over everything sc composes — so it survives every regeneration and survives re-running install.sh.
An override that is absent, empty, or {} changes nothing. One that cannot be applied stops the command before anything is written: config.json is left exactly as it was, the running service is not touched, and the message names the file and the problem.
Objects merge by depth. A key you do not mention keeps its value and its position:
{ "log": { "level": "debug" } }→ only log.level changes; every other key of log, and its position, stays as it was.
Arrays change only under an explicit directive, because "add one DNS rule" and "replace every DNS rule" must never look the same:
| Directive | Effect |
|---|---|
$replace |
the array becomes exactly the value you give |
$prepend |
your elements go in front of the existing ones |
$append |
your elements go after the existing ones |
$before |
your elements go immediately before the element matched by match |
$after |
your elements go immediately after the element matched by match |
$before / $after take {"match": {…}, "values": […]}. match selects by subset equality — every key/value in it must equal the element's — and must match exactly one element; zero or several is an error, never a silent no-op. Anchors rather than numeric indices, because sing-box evaluates dns.rules and route.rules in order and an index is wrong the moment anything inserts earlier.
Example — insert an AAAA-suppressing DNS rule immediately after the clash_mode: Direct rule:
{
"dns": {
"rules": {
"$after": {
"match": { "clash_mode": "Direct" },
"values": [
{ "action": "predefined", "rcode": "NOERROR", "query_type": [28] }
]
}
}
}
}Values you insert are copied verbatim: nothing inside them is re-interpreted, so an inserted rule carrying its own rule_set or domain_suffix array is emitted exactly as written. A bare array where the generated config already has one is refused with a message naming the directives; a bare array at a key the generated config does not have is simply accepted and creates it.
scdepends on parts of the config it generates. Removingexperimental.clash_api.external_controller, or renaming theproxyoutbound, yields a filesing-box checkstill accepts whilesc useandsc statusstop working.scdoes not stop you — it is your file.
If you already hand-edited config.json, the next command that regenerates it prints one line on stderr: that the file was changed outside sc, that the change is about to be replaced, and where to put it so it lasts. The comparison is against /etc/sing-box/.config.sha256, a digest of what sc last wrote; a host that has never run this version has no record yet, so nothing is printed until after its first regeneration.
Pick any:
sc uninstall # easiest, on installed systems
sudo ./uninstall.sh # in the repo dir
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/Alan-IFT/singbox-cli/main/uninstall.sh)" # one-line remoteThis wipes the service unit, /etc/sing-box/ (incl. nodes), /var/lib/sing-box/, /var/log/sing-box/, sudoers, /usr/local/bin/sc, /usr/local/lib/singbox-cli/. Then it asks whether to also remove the sing-box binary — answer y for truly zero residue.
nodes.jsoncontains node credentials/UUIDs, mode 600, root-only readable.config.jsonis generated fromnodes.jsonand embeds the same credentials, so it is mode 600 too. Both are written to a fresh file that is already mode 600 before its first byte, then moved into place, so neither is ever readable by anyone but root — not even for the instant it is being written. One consequence:sing-box check -c /etc/sing-box/config.jsonnow needs root, as it should.scuses sudoers NOPASSWD, scoped to/usr/local/bin/sconly.scis owned by root, regular users cannot modify it, so NOPASSWD cannot be bypassed.- For multi-user machines, consider switching NOPASSWD back to password-required.
PRs welcome. Top priorities:
- Subscription link auto-update
- urltest support beyond selector (auto-pick the fastest node)
- RHEL / Fedora / Arch family support
-
sc pingfor node latency testing - Node import/export (JSON backup)
MIT — see LICENSE.