How It Works (Internals)
Internal Architecture
Execution flow
bin/clash-verge.js (shebang)
└─► index.js (main)
├─► require('./constants') — reads paths
├─► process.argv — checks for --help
└─► child_process.spawn('sudo', [path])
└─► exec() with env { ...process.env, CLASH_VERGE_TUN: '1' }
Key code path — index.js (main logic)
const { spawn } = require('child_process');
const { CLASH_VERGE_PATH } = require('./constants');
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log('Usage: clash-verge-cli [--help]');
process.exit(0);
}
const child = spawn('sudo', [CLASH_VERGE_PATH + '/Contents/MacOS/Clash Verge'], {
stdio: 'inherit',
env: { ...process.env, CLASH_VERGE_TUN: '1' }
});
child.on('error', (err) => {
console.error('Failed to start Clash Verge:', err.message);
process.exit(1);
});
child.on('exit', (code) => {
console.log('Clash Verge exited with code', code);
process.exit(code);
});
Why sudo?
macOS requires root to create a utun (userspace TUN) interface. Without sudo:
Error: permission denied — cannot create TUN interface
The CLI wraps the binary in sudo so macOS asks for your password in the terminal instead of popping up a GUI dialog.
Why stdio: 'inherit'?
This forwards Clash Verge's stdout/stderr to your terminal. You'll see the app's startup logs. If you set it to 'pipe', those logs are suppressed.
Error handling
- sudo not found:
spawnemits'error'→ exits 1 with message - Wrong Clash Verge path:
spawnemits'error'(ENOENT) → exits 1 - Wrong password: sudo itself handles this — it prints "Sorry, try again" and exits
- Clash Verge crashes: exit code forwarded to your shell