rdsh is a drop-in fast path for dsh
(the DeepSeek Harness CLI). Instead of a full rewrite, it ports only the hot paths
to Rust and delegates everything else to the original dsh binary — so you get
~98x faster startup and ~1/23rd the memory with zero behavior change.
- Startup median ~0.90ms (original
dsh: ~88ms) - Resident memory ~2.9MB (original: ~66MB), single ~806KB binary, no runtime tree
- Safe by construction: agent loop and profile boot are never reimplemented,
delegation is a verbatim
exec, and every optimization is output-identical
- Benchmarks
- Install
- Usage
- Replacement mode (run as
dsh) - Using with Smart-DSH
- Web dashboard
- Safety design
- How it got fast
- Project layout
- Contributing
- FAQ
- Credits
- License
Measured on Linux x86_64, including before/after comparisons for the optimizations.
| Case | rdsh | Baseline | Factor |
|---|---|---|---|
--version startup (median, n=5) |
~0.90ms | original dsh ~88ms |
~98x |
--version peak RSS |
~2.9MB | original ~66MB | ~1/23 |
| Hook-equivalent peak RSS | ~2.7MB | equivalent Node script ~45MB | ~1/16 |
| search (300 files, ~600k lines) | ~17ms | before ~41ms | ~2.4x |
| tokens (9.6MB text) | ~12ms | before ~35ms | ~2.9x |
| sessions --tokens (20 sessions) | ~0.41s | before ~1.65s | ~4.0x |
| Distribution size | one ~806KB binary | ~508MB Node tree | — |
Reproduce with rdsh bench --n 5 and /usr/bin/time -v. The before/after
binaries were built from HEAD vs. the working tree in a scratch worktree and
their outputs were diffed for equality.
Fastest (prebuilt binary, no Rust needed):
# Linux / macOS / WSL
curl -fsSL https://github.com/sahenjp/rustdsh/releases/latest/download/install.sh | bash -s -- --from-release# Windows (PowerShell)
& ([scriptblock]::Create((Invoke-WebRequest -Uri https://github.com/sahenjp/rustdsh/releases/latest/download/install.ps1).Content)) -FromReleaseFrom source:
git clone https://github.com/sahenjp/rustdsh.git
cd rustdsh
./install.sh # build + install to ~/.local/bin/rdsh
./install.sh --as-dsh # also shadow `dsh` (original kept as dsh-orig)
./install.sh --restore # undo the shadowing
./install.sh --prefix=DIR # custom install dir (default ~/.local/bin)install.sh covers Linux, macOS, and WSL (auto-detects WSL, auto-installs
Rust via rustup unless --no-rustup). Native Windows uses install.ps1:
git clone https://github.com/sahenjp/rustdsh.git
cd rustdsh
.\install.ps1 # build + install to %LOCALAPPDATA%\rdsh\bin (+ user PATH)
.\install.ps1 -AsDsh # also shadow `dsh` (original kept as dsh-orig)
.\install.ps1 -Restore # undo the shadowing
.\install.ps1 -Wsl # also install inside WSL via install.sh| OS | script | notes |
|---|---|---|
| Linux / macOS | ./install.sh |
needs cargo or curl (rustup auto-install) |
| WSL | ./install.sh inside the distro |
detected automatically; alongside native via install.ps1 -Wsl |
| Windows (native) | .\install.ps1 |
needs Rust (winget install Rustlang.Rustup); MSVC build tools required to compile |
First boot with no model connected prints a pointer instead of leaving
you at the DeepSeek prompt: run rdsh setup (or rdsh setup --login to
start the Codex/opencode OAuth flow right away).
Or build directly: cargo build --release produces target/release/rdsh.
Requires Rust 1.73+ (uses u32::div_ceil, thread::scope); only three
dependencies (clap, serde_json, anyhow), no async runtime, no build scripts.
rdsh tui # same as: dsh --profile tui (with slim env)
rdsh --profile web --patch x.yml # boot with an extra overlay
rdsh --passthrough tui # byte-identical delegation, no slim env
rdsh --dry-run tui -- --resume abc # print what would be executedrdsh tokens ./AGENTS.md # estimate input tokens (~4 chars = 1, CJK = 1 each)
echo ... | rdsh prune --max-tokens 4000 # keep head+tail within a token budget
rdsh search TODO --dir . --max 100 # recursive grep (parallel, same order as sequential)
rdsh search-web "rust async" --limit 5 # web search via SearXNG (default http://127.0.0.1:8888, $SEARXNG_URL wins)
rdsh compact ./s.jsonl --max-tokens 8000 # compact a session transcript (source untouched)
rdsh sessions --limit 20 --tokens # list sessions with decompressed token estimates
rdsh logs --tail 50 --grep ERROR # inspect startup logs
rdsh profiles / rdsh skills # list profiles and skills
rdsh doctor # check original dsh, DSH_HOME, slim setup
rdsh bench --n 5 # compare rdsh vs dsh startup
rdsh serve # local web dashboard (:3080)Logins you already did elsewhere are mirrored into
$DSH_HOME/.credentials.yaml, the credential store dsh itself reads:
- Codex CLI (
~/.codex/auth.json, ChatGPT OAuth) - opencode (
$XDG_DATA_HOME/opencode/auth.json, e.g.openaiOAuth becomes theopenai-codexroute)
rdsh auth # status: what was found, what dsh already recognizes
rdsh auth --import # write missing/older grants (0600, other entries untouched)
rdsh auth --json # machine-readable status
rdsh setup # first-run wizard: import, DeepSeek-key paste, --login/--open
rdsh setup --web # floating glass setup UI on localhost (browser auto-opens)Booting (rdsh tui, dump-config, plugin) auto-syncs first, so logging
in with Codex/opencode is enough. RDSH_AUTH_AUTOSYNC=0 disables it.
A dsh-side token that is newer is never overwritten, and non-grant
records (API keys) are left alone.
guard scans stdin (hook JSON or raw text) for deny patterns and blocks on
match: exit code 2 with the reason on stderr, exit 0 otherwise. With --json
it prints {"decision":"block"} / {"decision":"approve"} instead. * in a
pattern matches any string. At ~1ms startup and ~3MB RSS, per-tool-call hook
cost is effectively zero.
echo "$input" | rdsh guard --deny "rm -rf /*" --deny "*token*"{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "rdsh guard --deny \"rm -rf /*\"" }] }
]
}
}This follows the dsh hook protocol: exit 2 blocks with a message the model sees, any other failure is non-blocking and only logged.
When the binary is invoked under the name dsh, anything that is not an
rdsh-native subcommand is delegated verbatim to the original binary, so
dsh --version, dsh --profile tui, and dsh --help stay byte-identical.
- Original-binary discovery order:
RDSH_ORIG_BIN(legacyDSH_ORIG_BINstill honored) →~/.config/rdsh/origin→ sibling backups (dsh-orig,dsh.orig,dsh.real) →PATH(self excluded) → known npm install paths - Naming follows dsh convention: kebab-case commands/flags like the original
(
dump-config,--from-default-profile), while theDSH_env namespace stays owned by dsh itself — rdsh-private keys live underRDSH_ - One-shot escapes:
RDSH_PASSTHROUGH=1 dsh ...(no slim env),RDSH_DRY_RUN=1 dsh ...(print only) - Name shadowing: a bare
dsh tokensruns the rdsh subcommand; a profile literally namedtokensstill boots viadsh --profile tokens - Node wrappers: scripts that run
node "$(... dsh ...)"break whiledshis shadowed (the path is now a native binary, not JS). Execdsh/rdshdirectly instead of vianode;rdsh doctorlists the offending wrappers.
Smart-DSH is a DSH web-profile plugin bundle (mobile UI, Web Push notifications, Esc-to-stop), not a competing binary — it coexists with rdsh. rdsh passes its setup commands through:
rdsh doctor # also shows dsh version + Smart-DSH bundles
rdsh --profile web --dump-config | grep notify-push # verify composition (read-only)
rdsh plugin --profile web add /path/to/dsh-notify-push # same as dsh plugin ...
rdsh --profile web # boot web with slim env (plugins unaffected)Co-use notes:
- Ports: the dsh web GUI and
rdsh serveboth default to 3080. Keep 3080 for dsh web (push/remote access) and runrdsh serve --port 38080. dsh-shadowing: withinstall.sh --as-dsh, Smart-DSH helper scripts that locate DSH viadshon PATH resolve to the Rust binary and fail. Run those scripts against the original (dsh-orig ...) or exportDSH_PACKAGE_DIRto the DSH package dir.- Versions: Smart-DSH documents DSH
0.1.2-rc.1;rdsh doctorprints your actual dsh version so mismatches are visible before installing bundles.
rdsh serve
# open http://127.0.0.1:3080/ (localhost only, read-only API)
# if the port is taken (the dsh web GUI also uses 3080), try --port 38080| API | Purpose |
|---|---|
GET /api/version |
version |
GET /api/doctor |
health check |
POST /api/tokens |
token estimate for {"text"} |
POST /api/prune |
prune {"text","max_tokens"} to budget |
GET /api/bench?n=3 |
startup measurement |
GET /api/sessions?limit=20 |
recent sessions |
GET /api/skills, /api/profiles |
name lists |
Dependency-free (std-only HTTP server plus one embedded HTML file, no CDN, works offline).
The optional Node.js dashboard adds project metrics,
tasks, human questions/replies, native MCP Events for ChatGPT Dots, and Tailscale
QR access. rdsh-dashboard project --project <directory> opens a project-specific
dashboard; rdsh-dashboard harness starts a separate original Harness Web UI.
See the guide for installation, MCP client configuration, and private Dots
connections through Secure MCP Tunnel. Requires Node.js 22+.
- The agent loop and profile boot are never reimplemented — delegation only.
- Slim mode only adds environment variables; unknown keys are ignored upstream.
- Launcher error cases from the original (
desktopprofile, mutually exclusive dumps, missing--profile) are reproduced in Rust. - Read paths never write: tokens/search/compact/dump/native APIs touch nothing.
- Instant retreats:
--passthrough,RDSH_PASSTHROUGH=1,./install.sh --restore.
cargo test: 26 unit tests pass (token math, wildcard matcher, arg splitter, auth splice/freshness, setup lang). The suite caught and fixed one real matcher bug (single-pattern substring).tests/regress.sh: 35 CLI checks pass (every subcommand, error paths, auth import round-trip, setup first-run flow, and sandboxeddsh-name delegation against a fake original).- Optimization diffs: old vs. new binary outputs compared byte-for-byte (300-hit search and truncated-max search both identical).
- Live replacement verified on a real machine:
dsh --versionstill delegates, new native commands work under thedshname.
- ASCII fast path for token estimation: pure-ASCII input is one
len/4computation (non-ASCII keeps the exact scan; results identical). - Two-phase search: sequential walk fixes the order, files are grepped in parallel, hits merge back in walk order. Trees under 32 files keep the exact old sequential code path.
- Parallel zstd expansion for
sessions --tokens(same numbers, order kept). - Release profile stays small:
opt-level=z, LTO,strip,panic=abort(~806KB).
src/main.rs— CLI definition, dispatch,dsh-name detectionsrc/auth.rs— OAuth auto-recognition (codex/opencode → credentials.yaml)src/dsh_args.rs— originallib/bin.js-compatible arg splitter (read-only)src/passthrough.rs— original-binary discovery +execdelegationsrc/slim.rs— slim environment definitionsrc/tokens.rs— token estimation and pruningsrc/search.rs— order-preserving parallel grepsrc/websearch.rs— SearXNG web search (search-web, no API key)src/compact.rs— session transcript compactionsrc/inspect.rs— read-only sessions/logs/skills/profiles viewssrc/guard.rs— hooks.json guard commandsrc/serve.rs+src/ui.html— local web dashboardsrc/setup_web.rs+src/setup.html— floating glass setup UI (setup --web)install.sh— installer (--as-dshshadow /--restore)tests/regress.sh— CLI regression suite (35 checks)
cargo fmt --check # must be clean
cargo clippy --all-targets -- -D warnings # must be clean
cargo test # 26 unit tests
BIN=./target/debug/rdsh sh tests/regress.sh # 35 CLI checks (needs cargo build first)No new dependencies without discussion: binary size and startup time are
features. Behavior changes must extend tests/regress.sh.
Release: git tag vX.Y.Z && git push origin vX.Y.Z builds per-OS binaries
(Linux/macOS/Windows) and attaches them to the GitHub Release via the cd
workflow.
- Port 3080 is busy? The dsh web GUI uses it too — run
rdsh serve --port 38080. - A profile collides with a subcommand name? Boot it explicitly:
dsh --profile <name>. - Revert the replacement?
./install.sh --restorebrings the original back. - What does
~123tok?mean? Without thezstdCLI the estimate falls back to compressed-bytes/4; the?marks that.
Ideas: @studio_yebisu, @remydre8.
MIT — see LICENSE.