Niubash
Bash, native on Windows. No WSL. No VM. No
/mnt/c. No cmdlet dialect. Just the shell your fingers already know — and the one your AI agent actually speaks.
This is the Niubash documentation. Start with the guides, or jump straight into the shell:
C:\Users\you\repo
❯ test -f Cargo.toml && echo "rust, obviously"
rust, obviously
❯ printf "%s\n" alpha beta gamma | grep beta
beta
❯ cd "C:\Program Files"
C:\Program Files
What Niubash is
- Bash syntax.
if,for,case,$(...), pipes, redirects, heredocs, functions, aliases — the whole grammar, powered by rubash, which is gated on the GNU Bash project's own 83-suite upstream test corpus (82 of 83 suites byte-identical, per the 2026-09-25 ledger re-run; the one residual, nameref, is individually audited — see the rubash compatibility ledger). - Windows-native. One binary, one process. Native Windows paths
(
C:\...andC:/...), direct execution ofgit.exe,node.exe,cargo.exe,python.exe— no VM, no emulation layer. The Windows-native path is the shell's first-class representation, POSIX- and WSL-style spellings are understood on input, and the Rubash executor translates POSIX-form arguments deterministically before a native child starts:/d/repoarrives atpython.exeasD:\repo, while shapes the executor does not classify as paths (/nonexistentand/fn main) arrive byte for byte. The native command processor keeps its/C-style switch-position operands verbatim. The full contract — the exact rules, thecmd.execarve-out, and the__RUBASH_ARGV_DIALECT=legacypass-through switch — is stated in the Windows Path Contract. - Unix commands included. WinuxCmd ships
ls,cat,grep,find,test,printf, and friends as Windows command links, with no separate installation. - AI-native.
niu -cis a contract: no banners, stable stdout/stderr, exact exit-code propagation. What an agent writes is what the process receives — the quoting roulette of PowerShell and the path mangling of MSYS/Git Bash are both gone. - A prompt you'll enjoy. 27 bundled themes (agnoster, spaceship, pure, catppuccin-mocha, tokyonight, p10 family, ...), a git prompt that grows teeth inside any repository, syntax highlighting, autosuggestions, vi and emacs modes, and Ctrl+R history search.
- Plugins with a permission model. 40+ bundled packs (
git,docker,kubectl,npm,zoxide,direnv,fzf,thefuck, ...), with reviewed source packs and process adapters declaring the host access they need.
The documentation
| Guide | What it covers |
|---|---|
| Getting Started | Zero to a git prompt in ten minutes |
| Advanced Usage | Execution modes, startup files, themes, plugins, completion, debugging |
| Install & Self-Update | Installer, portable zip, Windows Terminal profile, updates |
| Bash Compatibility Matrix | What Bash surface is verified, layer by layer |
| Built-ins & Fast Paths | All 64 built-ins, fast-path fallbacks, WinuxCmd overlap |
| Architecture | rubash + WinuxCmd + reedline, path model, host contract |
| Windows Path Contract | logical root, dispatcher selection, and layer ownership |
| Roadmap | What is done, what is next |
Get it
Download the installer from GitHub Releases, or build from source:
git clone https://github.com/unixwin/niubash.git
cd niubash
cargo build --release
target\release\niu.exe
Keep it current with niu --self-update (or self-update inside the
shell) and wpm update winuxcmd for the Unix command set. Questions and bug
reports go to
github.com/unixwin/niubash/issues.
Shell semantics live upstream in rubash — fix the engine, and every Bash user on Windows wins.
Why Niubash
The long version of the pitch. The README sells it in five seconds; this page shows the receipts.
The Windows shell civil war
Every Windows developer knows the tabs open on their machine:
| Option | The fine print |
|---|---|
| CMD | Frozen in 1987. Your company's docs still paste it. |
| PowerShell | A genuinely powerful automation language. It is not Bash — your for loops, grep, and quoting instincts die on arrival. |
| WSL | A whole Linux distribution as a hobby you didn't sign up for. Boot a VM to print a directory. Dock your Docker Desktop's memory. Greet /mnt/c/Users/you/.... |
| Git Bash / MSYS2 | Brilliant emulation with a cost: it translates, mangles, and converts your paths at the worst possible moments, and every git pays an emulation tax. |
The treaty
Niubash is one native Windows binary that runs Bash syntax on Windows paths
with real Windows programs — and brings the Unix commands Windows never had
(ls, cat, grep, find, test, printf, ...).
| Niubash | WSL | Git Bash | PowerShell | |
|---|---|---|---|---|
| Bash syntax | ✅ | ✅ | ✅ | ❌ |
Native Windows paths (C:\..., no /mnt/c) | ✅ | ❌ | ⚠️ conversion quirks | ✅ |
Calls git.exe / node.exe directly | ✅ | ⚠️ via /mnt/c | ⚠️ path translation | ✅ |
Unix commands (ls, grep, find) | ✅ | ✅ | ✅ | ❌ |
| Cold start to prompt | ~170 ms | seconds | ~1 s | ~280 ms |
| No extra OS, no VM | ✅ | ❌ | ✅ | ✅ |
| Themes, git prompt, plugins | ✅ | — | ✅ | ⚠️ |
One binary. One process. No distro to patch, no emulation layer to appease.
PowerShell eats arguments
Windows itself is the root cause: the platform has no argv array. Every executable receives a single command-line string and parses it with its own rules — and PowerShell adds a second parser on top. That's how PowerShell/PowerShell#1995 ("Arguments for external executables aren't correctly escaped") became a household name, and how dotnet/runtime#23347 opens by citing "the ongoing quoting woes PowerShell experiences."
Same command line, two shells. Watch what actually reaches the process:
# PowerShell 5.1 # Niubash
> node -e "console.log(JSON.stringify( ❯ node -e "console.log(JSON.stringify(
process.argv.slice(1)))" "a b" "" "c\"d" process.argv.slice(1)))" "a b" "" "c\"d"
"e\f" "---" "e\f" "---"
ParserError: TerminatorExpectedAtEndOfString ["a b","","c\"d","e\\f","---"]
That command is written the way any model trained on Bash would write it. PowerShell throws a parse error before the process even starts. And when you quote it PowerShell's own way, the damage is quieter but still real: five arguments in, two mangled ones out — the empty string vanishes, the embedded quote is flattened, the last two arguments fuse into one:
> node -e "console.log(JSON.stringify(process.argv.slice(1)))" "a b" "" 'c"d' "e\f" "---"
["a b","cd e\\f ---"]
Agent casualties, with receipts
For humans this is a nuisance. For AI agents it's a minefield: models are trained on Bash, their quoting instincts are Bash-shaped, and on a PowerShell machine every generated command is a roll of the dice.
- anthropics/claude-code#65162 — agent used a PowerShell here-string (
@'...'@) inside the Bash tool; the git commit message was silently corrupted, andgit commitstill exited 0. - anthropics/claude-code#83243 — trivial commands fail with
unexpected EOFon Windows when PowerShell is the primary shell. - anthropics/claude-code#76490 — Bash allow-list rules never match Windows drive-letter paths; every run re-prompts.
- openai/codex#31548 — Windows users are voting to make the Codex agent shell configurable; it is locked to PowerShell today.
- openai/codex#35995 — Python works in the interactive shell but silently disappears inside Codex task execution.
- openai/codex#33891 / openai/codex#36560 — the ChatGPT desktop app flashes PowerShell windows while its agent works.
- microsoft/vscode-python-debugger#233 — a space in a path yields incorrectly quoted debug commands.
The path philosophy: native in, native out
If your mental model says every Unix-flavored shell on Windows must contain a path-translation layer — because every one you've met so far did — that is the model running on precedent, not a fact about this product. In niubash, the Windows-native path is the first-class citizen: not the target of a conversion, but the representation the shell actually thinks in.
Windows-native binaries don't speak the MSYS dialect. Here is Git Bash, in the wild, failing at exactly the things Niubash does without thinking:
What just happened, command by command:
| Command | In Git Bash (MSYS) | In Niubash |
|---|---|---|
node -e "..." "C:\Users\you\repo" | the backslashes are eaten — the argument vanishes | argument arrives byte-for-byte |
cmd /c dir /c/Windows/... | cmd.exe rejects the MSYS path (exit 1) | native path, native tools, no guessing |
git grep "/fn main" | MSYS rewrites /fn into a Windows path — the pattern silently dies | / is just a character, not a path to convert |
The escape hatches are the confession
MSYS "solves" paths by guessing: it heuristically rewrites /c/foo into
C:\foo, and famously mangles innocent arguments along the way — the
notorious case of git grep "/pattern" silently becoming
git grep "C:\...\pattern". A heuristic that always guesses right does not
exist, which is precisely why Git for Windows ships MSYS_NO_PATHCONV and
MSYS2 ships MSYS2_ARG_CONV_EXCL: an off switch for path conversion is a
documented admission that the conversion layer breaks real commands. Under
MSYS, every native tool call is a game of path roulette, and the house
provides the abort switch next to the wheel.
Niubash keeps the boundary but changes its shape. The translation is
deterministic and lives in niu's own Rubash executor, not in an emulator
rewriting arguments behind the child's back: /d/repo reaches a native
child as D:\repo, /nonexistent and /fn main pass through byte for
byte, and the mapping does not check whether the path exists. The same
principle holds for the rest of the path surface: the Windows-native path
is not a format niubash converts into — it is the shell's internal
representation. POSIX-style (/c/..., /usr/bin),
WSL-style (/mnt/c/...), and Windows-style (C:\...) are input spellings
that all resolve to that one native reality:
❯ cd /c/Users/you/repo # MSYS-style input: understood
C:/Users/you/repo # output: always Windows-native
❯ node -e "console.log(process.cwd())"
C:\Users\you\repo # native binaries get native cwd
❯ cd "C:\Program Files" # Windows-style input: understood
❯ pwd
C:/Program Files
Input in any dialect. Output always native. One representation, zero
guessing: the executor's POSIX→native argument mapping is fixed and
documented (windows-path-contract.md), and the
one switch is __RUBASH_ARGV_DIALECT=legacy, which hands argv to the
child unwritten — a dialect selector for a documented mapping, not the
abort switch of a heuristic that might guess wrong. That is the honest
version for your fingers, and for your agent.
The verdict
Every incumbent made a bet with the tools of its decade: CMD froze,
PowerShell bet on a brand-new language, WSL bet on virtualization, and
MSYS2/Cygwin bet on emulation. For the actual problem — running Bash on
Windows, next to native Windows programs — three of those bets pay out
nothing, and the fourth is structurally guaranteed to leak: translating
between two path worlds is a guessing game, and guesswork embedded in
infrastructure surfaces at the worst possible moment. The escape hatches
(MSYS_NO_PATHCONV, MSYS2_ARG_CONV_EXCL) are the incumbents' own
documentation of that fact.
The correct architecture is not a better emulator. It is no emulator: a native implementation where the Windows path is the only representation that exists, where Bash is implemented as a language — and proven against GNU Bash's own 83-suite upstream test corpus (82 suites byte-identical as of the 2026-09-25 ledger re-run, with the single nameref residual individually audited — see the rubash compatibility ledger) — and where native programs are called as what they are: native. That is what niubash ships, and it is why the emulation stack's failure modes are not "reduced" here. They are categorically absent.
Emulation was the previous century's approximation of this architecture. Niubash is the architecture.
Coding agents on Windows, and the shell they run
Your agent does not run commands in your shell. It picks one, and on Windows that pick is made by a probe you can usually influence. This page lists the mechanisms, the agent-by-agent behaviour, and what niu has to provide.
Verified against the machine in docs/ (2026-10-16): niu 1.4.0, niu-git
2.55.0.5 (flat layout), Git for Windows 2.50.1 at D:\Git, a WSL2 distro
installed, opencode, codex (forked), ZCode. Rows that were read from shipped
code or vendor docs say so; rows that come from upstream issue threads are
marked as reported.
The one thing that usually just works
Launch the agent from a niu session:
niu
$ opencode # or codex, or any CLI agent
niu exports SHELL=<path to niu.exe>. Agents that follow the POSIX convention
read $SHELL and use it. That is how the agent driving this document runs: its
process tree is powershell.exe <- niu.exe <- opencode.exe, and opencode's
shell selector is literally
function o$(e, r) { return Xc(process.env.SHELL, void 0, r, e) ?? n$(e); }
// ^ which("bash") ?? "/bin/sh"
opencode has no niu-specific code at all (zero occurrences of niu in the
binary) and no config field for the shell; $SHELL is the whole mechanism.
The catch: $SHELL is not persisted by anything. It exists only in
processes descended from a niu session. Start the agent from the Start menu or
Explorer instead and $SHELL is empty, so the same agent falls through to
which("bash"), which on a machine with WSL resolves to
C:\Windows\System32\bash.exe and runs your commands in Linux.
The four mechanisms
| # | Mechanism | Reads | Covered by launching from niu |
|---|---|---|---|
| 1 | $SHELL (POSIX convention) | the environment | yes |
| 2 | Explicit per-agent setting | config file / CLI flag | n/a, set it once |
| 3 | Git for Windows probe | hardcoded paths, or derived from git | no |
| 4 | Bare name, a table, or PowerShell by default | PATH, ComSpec, PATHEXT | partly |
Mechanism 3 is why the install places a bash.exe at the Git for Windows
location; mechanism 4 is why a bare bash cannot be redirected from the user
PATH alone.
What winuxcmd\bin\bash.exe actually is
It is not niu. It is a launcher that starts niu.exe as a child process, so an
agent pointed at it runs agent -> bash.exe -> niu.exe. Two consequences, both
filed:
- The extra process costs about 17.6 ms per
-cinvocation (niubash#377). One process hop is a rounding error for an interactive session and a real tax for an agent that shells out per command. - Because the child is an ordinary console program, a console-less parent used
to get a visible console window per command. That is niubash#379: Claude
Code pointed at this launcher flashed a window for every command. Fixed in
rubash#668 (
eaa6138) by settingCREATE_NO_WINDOWwhen all three standard streams are redirected; it reaches users with the next rubash bump and release build.
Both disappear if bash.exe becomes an argv[0] dispatch name for niu.exe
itself rather than a launcher, which is the same change that cheapens the hop
(niubash#377).
Which shell is my agent actually using?
Ask the agent to run this and compare the signatures:
echo "SHELL=$SHELL"; uname -s; uname -a
| What you see | Shell | Notes |
|---|---|---|
uname -s = MSYS_NT-..., uname -a ends x86_64 Msys | niu (winuxcmd shim) | what you want; $SHELL points at niu.exe |
uname -s = MINGW64_NT-..., MSYSTEM=MINGW64 | Git Bash | Git for Windows' own runtime |
uname -s = Linux | WSL | the surprising one; the agent reached System32\bash.exe |
uname not found, $SHELL empty | PowerShell or cmd | check $PSVersionTable.PSVersion vs %COMSPEC% |
The same check appears in Cursor's own support threads, where the fix is
verified by uname -a ending in MINGW64_NT-... instead of
microsoft-standard-WSL2.
Interpreting the result:
Linuxmeans the agent fell through towhich("bash"). Fix the mechanism that agent uses, below.MINGW64_NTmeans it found Git Bash. If you want niu instead, set the agent's explicit shell (mechanism 2) or the$SHELLvariable.- Empty
$SHELLwith PowerShell means the agent defaults to PowerShell, which is safe but not bash-compatible.
Agent reference
opencode
- Mechanism:
$SHELLfirst, thenwhich("bash"), then/bin/sh. Itsgitbashmode is implemented as "use$SHELL". No config field; the V2 CLI settings and core config have no shell key. - Set it to niu: launch from niu, or persist
SHELL. Nothing else is read. - Failure mode: falls to
which("bash")->System32\bash.exe-> Linux. - Separate code path: the self-updater resolves
git --exec-pathup three levels plusbin\bash.exeand refuses to update with "Git Bash is required to update OpenCode" when that file is missing. It does not consult$SHELL. See niubash#376. - Source: shipped binary, read locally.
Claude Code
- Mechanism: vendor docs. Without
CLAUDE_CODE_GIT_BASH_PATHit looks forbash.exein (1)C:\Program Files\GitandC:\Program Files (x86)\Git, then (2) thegitonPATH, usingbin\bash.exefrom that installation. It skips agitsitting in the launch directory or below it, or undernode_modules/.venv/env. - Set it to niu: either works today. Probe (1) hits the
bash.exethe installer places inC:\Program Files\Git\bin, or setCLAUDE_CODE_GIT_BASH_PATHinsettings.json. Both point at the launcher described above, which is why this configuration is the one that exposed niubash#379 (a visible console window per command, fixed in rubash#668). When updating, verify the flash is gone and interactive use is unchanged. - Trap: the variable only accepts a file named
bash.exe,sh.exe,bashorsh. Point it atgit-bash.exeand it is ignored with a warning. Pointing it atniu.exeis likewise ignored, so the launcher hop cannot be skipped by naming niu directly; that is another reason theargv[0]dispatch in niubash#377 matters for this agent specifically. - Failure mode: since v2.1.120, absent Git Bash means PowerShell, not Linux.
- Source: vendor documentation.
Codex
- Mechanism: a shell table with per-shell candidate paths. Windows entries
include
bash -> /bin/bash,sh -> /bin/sh,pwsh,powershell,cmd. - Set it to niu: this fork adds
ShellType::NiuwithC:\Program Files\Niubash\niu.exe,C:\Program Files (x86)\Niubash\niu.exe,%LOCALAPPDATA%\Programs\Niubash\niu.exeand aniulookup onPATH; the exec policy also whitelistsniu.exe -C,-c,-lc,--repl-command. This is the shape the other agents need. - Source:
codex-rs/shell-command/src/shell_detect.rs, read locally.
Cursor
Two products, both currently unhelpful:
- IDE agent: ignores
terminal.integrated.defaultProfile.windowsandterminal.integrated.automationProfile.windowsand runs PowerShell; the workaround is enabling the Legacy Terminal Tool setting. Some versions pick WSL bash instead, which is why their threads recommend nulling the PowerShell and WSL profiles withterminal.integrated.useWslProfiles: false. - Agent CLI: hardcoded to PowerShell. No
--shellflag, no shell field in~/.cursor/cli-config.json, and$SHELL/$COMSPECignored. The shippeddetectShellType()putsisWindows && (commandExists("pwsh") || commandExists("powershell"))in the same||chain as the string match, so on Windows the PowerShell arm fires regardless of the hint and every later branch is unreachable. A workingNaiveTerminalExecutorexists but nothing routes to it. - Set it to niu: not possible today through configuration. Reported upstream; nothing to do on the niu side until they expose a shell path.
- Source: upstream issue threads quoting the shipped detection code.
VS Code, and everything built on it
Cursor aside, the VS Code family is one shared probe:
var HX = ["C:\\Program Files\\Git\\bin\\bash.exe",
"C:\\Program Files (x86)\\Git\\bin\\bash.exe"];
// then: which("git") -> dirname(git) + "/../bin/bash.exe" and "/../../bin/bash.exe"
- Applies to: VS Code (Copilot agent), Cursor, Windsurf, Trae, ZCode, and extensions that use the editor's terminal or automation profile (Cline, Roo Code, Kilo Code, Continue).
- Set it to niu: probe 1 hits the installed
C:\Program Files\Git\bin\bash.exe. For explicit control, point the profile at niu:terminal.integrated.automationProfile.windows={"path": "<niubash>\\niu.exe", "args": []}. Agents that shell out through the automation profile inherit it. - Failure mode: PowerShell, or cmd if neither is configured.
- Source: ZCode
app.asarread locally; the rest is the same lineage.
Gemini CLI
- Mechanism:
getShellConfiguration()returns PowerShell unconditionally on Windows.ComSpecis only consulted to decide betweenpwshandpowershell.exe; there is no bash branch. - Set it to niu: not possible in released code. Two upstream proposals
exist: detect MSYS via
process.env['MSYSTEM']and route tobash -c(issue #26567), and prefer Git Bash by searchingPATHfor abash.exewhose path containsgit(PR #18367). - Failure mode: PowerShell, and on locked-down machines an execution-policy error that stops the agent entirely.
- Source: upstream source and issue threads.
DSH (DeepSeek Harness)
- Mechanism: plugins.
dsh-niuships a profile bundle that enablesniu-sandboxandtool-bash, disables the PowerShell shell/tool rows, and adds a settings card. A third party shipsdsh-niubash-onlyin the same spirit. - Set it to niu:
dsh plugin --profile web add @cmx666/dsh-niu-bundle@<version>. - Source: the plugin READMEs.
Zed
- Mechanism: explicit.
terminal.shell.programnames the shell, and the agent uses that terminal. - Set it to niu: point
terminal.shell.programatniu.exe. - Source: settings in use on this machine.
Aider
- Mechanism: there is no shell selection option.
/runuses PowerShell orcmd.exeon Windows, and the model-visible environment reportsShell: COMSPEC=....--suggest-shell-commandsand--shell-completionsare unrelated. - Set it to niu: not supported. Launching Aider from niu does not change
the shell it uses for
/run. - Failure mode: cmd/PowerShell command syntax, not a bash failure.
- Source: upstream options reference and changelog.
Not verified here
Amazon Q Developer CLI, Goose, Amp, OpenHands, Devin, and editor agents outside the VS Code lineage were not inspected on this machine and have no entry yet. Add them when someone can read the shipped selector.
What niu must provide, and who owns each path
| Path | Serves | Owner | Needs admin |
|---|---|---|---|
SHELL=%LOCALAPPDATA%\Programs\Niubash\niu.exe (user env) | mechanism 1 | installer | no |
%LOCALAPPDATA%\Programs\Niubash\bin\{bash,sh}.exe | opencode-shaped derivation when git resolves from winuxcmd\bin | installer / niu setup | no |
C:\Program Files\Git\bin\bash.exe | mechanism 3 probe 1 (Claude Code, VS Code family) | installer | yes |
C:\Program Files (x86)\Git\bin\bash.exe | the second hardcoded candidate | installer | yes |
C:\Program Files\Git\bin\sh.exe | tools that want sh beside the bash they found | installer | yes |
...\winuxcmd\opt\niugit\bin\{bash,sh}.exe | opencode's updater probe (git --exec-path up three) | niugit package manifest, so wpm refresh keeps it | no |
The entry directory ...\winuxcmd\bin must contain only names you intend to
own globally (git, sh, bash), because it is what goes on PATH.
Why PATH cannot win against System32
Windows resolves a bare name handed to CreateProcess by searching the
application directory, the current directory, the system directory, the
Windows directory, then PATH. Measured with the entry directory first on
PATH:
| Caller | bash becomes |
|---|---|
cmd /c bash -c 'uname -s' | winuxcmd's bash (MSYS_NT-...) |
PowerShell & bash | winuxcmd's bash |
Python subprocess(['bash', ...]) | WSL (Linux) |
Shells resolve names themselves, so PATH order decides for them. A process
that passes the bare name to CreateProcess never sees PATH before the system
directory, so a bare bash cannot be fully taken over from a per-user PATH.
The complete answers are the Git for Windows path (C:\Program Files\Git\bin\bash.exe)
and telling callers to use sh, which has no System32 counterpart.
Recommended order of work
- Installer writes
SHELLto the user environment, and removes it on uninstall. One registry value, no elevation, and it covers the largest class of agents including every POSIX-flavoured CLI. Users who launch from niu already have it; this makes it independent of the launch method. niu setup agents(idempotent) creates the non-elevated paths, prints what each probe resolves to, and says which entries need elevation.- Installer's optional elevated step creates the
Program Files\Gitentries, with the second candidate andsh.exe. - Per-agent settings for the agents that read neither
$SHELLnor the Git probes: Zed, DSH (done), the Codex fork (done), the VS Code automation profile.
Two things not to do: do not add winuxcmd\usr\bin to PATH (hundreds of
names, including ones System32 owns), and do not replace
C:\Windows\System32\bash.exe.
Known traps
MSYSTEMis empty in a niu session. Tools that sniff MSYS through it (Gemini CLI's proposed fix) will not see niu. Setting it is untested and changes how other MSYS-aware tools behave, so treat it as an experiment.unamein the winuxcmd shim reportsMSYS_NT-.... Anything that branches onunamewill classify the host as MSYS, which is deliberate for compatibility but visible to autotools and similar.- Claude Code accepts only
bash.exe/sh.exe/bash/shinCLAUDE_CODE_GIT_BASH_PATH. - A
bashthat resolves to Windows' WSL launcher starts Linux bash, and there is no error: the agent simply runs your commands on another OS.
Getting Started with Niubash
A short walkthrough from zero to a working prompt with git status.
1. Build or download
git clone https://github.com/unixwin/niubash.git
cd niubash
cargo build --release
After building, the binary is at target\release\niu.exe. You can run it
directly, or add target\release to your user PATH using your normal Windows
environment settings:
target/release/niu.exe
If you are using the release zip, niubash automatically runs the activation script on first start when command links are missing:
niu winuxcmd/activate-winuxcmd.sh
That creates local command links inside winuxcmd/, so ls, cat, and
friends resolve normally. Once the links exist, startup skips activation.
2. Start the shell
niubash
You should see something like:
user@DESKTOP C:\Users\you
%
Type exit or press Ctrl+D to quit.
3. See the git prompt
cd into any git repository:
cd C:\Users\you\repo
# if inside a repo, the prompt changes:
user@DESKTOP C:\Users\you\repo git:(main) ●1 ✚2 ?1
%
Symbols at a glance:
| Symbol | Meaning |
|---|---|
●N | N files staged for commit |
✚N | N files modified but unstaged |
?N | N untracked files |
↑N | N commits ahead of upstream |
↓N | N commits behind upstream |
⚑N | N stashes saved |
✖N | N merge conflicts |
The branch name is green when the tree is clean, yellow when dirty.
4. Try some commands
pwd # prints C:/Users/you/repo
ls -la # Unix-style listing
echo "hello from $USER"
for i in 1 2 3; do echo $i; done
if [ -f Cargo.toml ]; then echo "yep"; fi
cat Cargo.toml | grep name
grep -n "fn main" src/main.rs
Windows paths work directly:
ls C:\Windows\System32\drivers\etc
ls D:/Projects
cd "C:\Program Files"
Multiline blocks work naturally:
for f in *.toml; do
echo "found $f"
done
5. Try git completions
git ad<Tab> # completes to `git add`
git commit -<Tab> # shows flags: --message, --all, --amend
git push --fo<Tab> # completes to --force
git branch -<Tab> # shows -d, -D, -m, -v, -a, -r
6. Set up your config
Create ~/.niubashrc for interactive shell code — environment, aliases,
functions — plus optional defaults and theme enablement:
# Optional floor knobs: they only shape the built-in default prompt and
# completion UI. An external theme claims PS1 and overrides the floor.
# NIU_PROMPT_CWD_STYLE='home' # home | full | basename
# NIU_COMPLETION_STYLE='gnu' # gnu (default) | ide | column | list | inline
if [ -z "${HOME:-}" ] && [ -n "${USERPROFILE:-}" ]; then
HOME="$USERPROFILE"
export HOME
fi
export EDITOR=vim
alias ll='ls -la'
alias la='ls -a'
alias gst='git status'
alias gco='git checkout'
alias gl='git log --oneline --graph --decorate --all'
hello() {
echo "hello from niubash"
}
~/.niubashrc is sourced only for the interactive REPL and the -C
one-shot REPL command path. It does not run for niu -c ..., script files,
or stdin script execution, so agent and CI surfaces stay deterministic.
~/.winshrc is a legacy compatibility fallback and is used only when
~/.niubashrc is absent. Plugin CLI enable/disable records, migration blocks,
completion overrides, test isolation, and advanced machine state are managed
internally. Prefer ~/.niubashrc for normal interactive customization.
6b. Prompts and themes
The built-in prompt is a floor, not an identity: it renders only while
nothing claims PS1. Anything that sets PS1 — your own line in
~/.niubashrc, an enabled oh-my-bash theme, or eval "$(starship init bash)"
— owns the prompt completely, and disabling it lets the built-in floor
render again.
The floor itself has one knob, NIU_PROMPT_CWD_STYLE (home by default;
also full or basename), set in ~/.niubashrc:
NIU_PROMPT_CWD_STYLE=full
Themes live in the external ecosystem (see the next section) — enable one
and its PS1 takes over the prompt; disable it and the floor comes back.
7. Plugins: the external ecosystem
Themes and plugins come from external plugin managers, installed and
enabled through niu plugin. The curated catalog has oh-my-bash,
bash-it, and bash-completion; any git URL, owner/repo shorthand, or
local path works too. New sources are untrusted by default — nothing
they ship runs until you review and trust them:
niu plugin add oh-my-bash # install from the catalog (untrusted)
niu plugin trust oh-my-bash # review, then pass the execution gate
niu plugin enable oh-my-bash # writes a guarded loader block into ~/.niubashrc
niu plugin enable agnoster # a theme asset -> sets OSH_THEME in that block
niu plugin disable oh-my-bash # removes the block; the built-in floor returns
niu plugin enable/disable maintains one managed block per source between
# >>> niu source <id> / # <<< niu source <id> markers at the end of
~/.niubashrc; everything outside the markers is yours and is never
rewritten. Use niu plugin list and niu plugin discover for the current
inventory and state.
Executable CLI tools (starship, fzf, eza, bat, fd, ripgrep, zoxide, dust,
duf, erdtree, direnv, niu-git …) install through your real package
managers — niu downloads nothing (download retraction, owner ruling
2026-10-04). niu plugin add fzf prints the commands:
niu plugin add fzf # prints: wpm install fzf (Windows, first choice),
# winget/scoop alternatives, apt/dnf/brew,
# plus the upstream release URL
Two systems coexist: the Unix command layer (ls, cat, grep, … plus
the application tools above) is owned by wpm on Windows (wpm install <tool>, wpm links rebuild, wpm update winuxcmd) — wpm is the
first-class tool channel there (owner correction 2026-10-03); on Linux and
macOS the native package managers (apt/dnf/yum/brew) own that plane.
niu plugin owns the bash-ecosystem extension plane (git sources, themes,
completions) on every platform.
What next
- Plugin System Direction for the v3 plugin model
- Plugin System Roadmap for the execution sequence
- Oh My Niubash Ecosystem for the external plugin ecosystem design
- Roadmap to see what is planned
- Source at github.com/unixwin/niubash
Advanced Niubash Usage
This guide covers the surfaces that matter after the first successful launch: execution modes, startup files, prompt/theme plugins, command discovery, and update/debug workflows.
For first-time setup, start with Getting Started.
Execution Modes
Niubash has three intentionally different execution paths:
niubash # interactive REPL
niu -c 'pwd; echo "$SHELL"' # quiet script/CI command mode
niu -C 'alias ll; pwd' # one-shot REPL command
- Use the interactive REPL for normal shell work.
- Use
-cfor scripts, tests, CI, and coding agents. It does not load~/.niubashrc,~/.winshrc, prompt plugins, or interactive lifecycle hooks. - Use
-Conly when a one-shot command needs the same startup state as the interactive REPL. It loads~/.niubashrcand lifecycle hooks, then exits.
This separation keeps automation deterministic while still allowing a rich interactive shell.
One-shot init file: NIU_ENV / BASH_ENV
By default -c loads nothing — no rc, no plugins, no hooks. If your script,
CI job, or coding agent needs shell aliases, exported variables, or PATH
tweaks, point NIU_ENV (or bash-compatible BASH_ENV) at a dedicated init
file. Only that one file is sourced, so the interactive-only content in
~/.niubashrc stays out of the one-shot path.
# ~/.opencode.env — a minimal init file for agents
export PATH="$HOME/tools:$PATH"
alias ll='ls -la'
export DOCKER_CONTEXT=my-cluster
NIU_ENV=~/.opencode.env niu -c 'll | head'
# or bash-compatible:
BASH_ENV=~/.opencode.env niu -c 'echo "$DOCKER_CONTEXT"'
NIU_ENV takes precedence over BASH_ENV when both are set. A leading ~
expands to your home directory. If the file does not exist, niubash prints
an error to stderr and continues — matching GNU bash's BASH_ENV behavior.
With neither variable set, -c stays zero-load and fast.
Startup And Config
Use ~/.niubashrc as the normal human-authored entry point:
# Optional floor knobs — they only shape the built-in default prompt and
# completion UI; an enabled external theme claims PS1 and wins.
# NIU_PROMPT_CWD_STYLE='home'
# NIU_COMPLETION_STYLE='gnu' # gnu | ide | column | list | inline
alias ll='ls -la'
export EDITOR=vim
# starship (or any tool that sets PS1) owns the prompt while it is active:
# eval "$(starship init bash)"
Themes and plugins are enabled through niu plugin, which appends managed
blocks at the end of the rc (see Plugin Workflow); your
lines outside those blocks are never rewritten.
The legacy files still exist, but they should not be the primary user path:
~/.winshrcis a fallback only when~/.niubashrcis absent.- Plugin CLI records, migration blocks, bundle versions, tests, and advanced overrides are internal managed state, not a user configuration file.
Do not put automation-critical behavior only in an interactive rc file. Pass
needed environment variables directly to niu -c or the script process, or
use NIU_ENV / BASH_ENV to source a dedicated one-shot init file (see
One-shot init file).
Completion Styles
TAB completion comes in five shapes. The default since 1.4.1 is gnu
(GNU readline factory behavior); before that it was ide. Pick one with
niu config set completion.style <name> (persisted in
~/.niubash/config.toml) or NIU_COMPLETION_STYLE=<name> in
~/.niubashrc or the environment — the env variable wins over the
persisted value:
gnu(default) — GNU readline: TAB completes the longest common prefix (a lone candidate completes outright); pressing TAB again on an unchanged line prints the candidates as width-adaptive columns next to the prompt and repaints it. The list is printed output, not a menu: nothing is selected, arrow keys stay on history, no popup.ide— multi-column popup with descriptions, VS Code style. Column count adapts to the terminal width; the description of the selected candidate renders beside it.column— plain multi-column grid (like zshcompinit).list— vertical list with descriptions (like fish).inline— first match inserted directly, Tab cycles (like bashmenu-complete).
show-all-if-ambiguous / show-all-if-unmodified-style readline variables
are not implemented yet; the double-TAB listing is the fixed shape.
Flag completions (ls --a<Tab> → --all with its description) come from the
bundle's generated completion assets and work in every style; ide and
list show the descriptions inline.
WinuxCmd Applet Completions
Every WinuxCmd applet ships with flag completion built into the niu binary:
grep --col<Tab> offers --color / --colour with descriptions,
ls --color <Tab> offers always / auto / never, and wpm <Tab>
lists the package-manager subcommands (install, search, uninstall,
...).
These definitions are generated from winuxcmd --help (WinuxCmd 1.1.5
transcripts) by scripts/generate-winuxcmd-completions.py, stored as one
TOML per applet under crates/niubash-runtime/assets/completions/winuxcmd/,
and compiled into the runtime — so they work on a fresh install with no
plugin enabled. When WinuxCmd gains applets or options, regenerate against
the new binary:
python scripts/generate-winuxcmd-completions.py --winuxcmd <path-to-winuxcmd.exe>
python scripts/test-winuxcmd-completions.py # golden check: corpus vs committed assets
A second compiled layer covers the common external commands that are not
WinuxCmd applets — git, tar, curl, ssh, docker, kubectl, and npm (issue
#305): static subcommand/flag tables under
crates/niubash-runtime/assets/completions/common/, so git co<Tab> offers
checkout/commit/config and kubectl get -<Tab> offers -o/
--all-namespaces on a fresh install.
Priority is layered: the embedded applet and common-command defaults are the
base layer, and completion definitions loaded later override them per
command — a bundle/pack TOML or a user completion-dir TOML whose file stem
(or command key) matches the built-in name replaces the built-in
definition entirely, so trimming or extending a command's flags is a plain
file drop, not a fight with the defaults.
AI Agent Skill Bundle
For AI hosts (Claude, ZCode, Cursor, ...) that drive your shell, niubash
ships a skill bundle describing its capability surface and dialect
contract: skills/niubash/SKILL.md plus references/. Install it without
hunting for the zip:
niu skill install # default: ~/.claude/skills/niubash/
niu skill install --target all # claude + zcode + cursor
niu skill install --target C:/agents/skills # generic: <dir>/niubash/
niu skill status # current / outdated per target (byte compare + sha256)
niu doctor reports an advisory agent skill row. The released
niubash-skill-v*.zip contains the same tree (WinuxCmd-skill layout), so
unpacking the zip into an agent skills directory is equivalent.
The command tables inside the bundle are generated — hand-written lists
rot. scripts/generate-skill.py fills the GENERATED-marked regions of
SKILL.md and references/quickref.md from the engine's own surfaces:
a captured help -s '*' transcript (builtin table), a captured
niu --help transcript (launcher/plugin verbs), and the applet completion
inventory (180 winuxcmd applets). After changing the launcher help or the
builtin table, refresh and re-check:
cargo build && python scripts/generate-skill.py --capture --niu target/debug/niu.exe
python scripts/test-skill-bundle.py # golden check: regions + embed manifest
Prompt And Themes
Defaults are a floor, not an identity. The built-in prompt (a reedline
template: user@host cwd symbol) renders only while nothing claims PS1.
Whoever sets PS1 last owns the prompt completely — your own rc line, an
enabled oh-my-bash theme, or starship:
PS1='\u@\h \w \$ ' # your own prompt: simplest possible claim
eval "$(starship init bash)" # starship claims PS1 on its first prompt
Disabling the claim (niu plugin disable oh-my-bash, or a plain
unset PS1) lets the built-in floor render again. PROMPT_COMMAND alone
is not a claim — it is a pre-prompt hook; hooks that want the prompt set
PS1 themselves.
The floor itself is shaped by NIU_PROMPT_CWD_STYLE (home, full, or
basename; see Getting Started). External themes
style themselves through the bash-compatible PS1 channel — colors,
powerline glyphs, and git segments come from the theme, not the shell.
History Modes
Set NIU_HISTORY_MODE in ~/.niubashrc when multiple shells share a
history file:
NIU_HISTORY_MODE=private
export NIU_HISTORY_MODE
shared(default) refreshes navigation from other shells.sessionkeeps the startup snapshot stable for navigation while builtins can observe later file updates.privateloads the complete history file at startup, then keeps later navigation changes local to the current shell while appending its own commands.
Custom Key Widgets
A shell function can act as a line editor widget. Declare it in
~/.niubashrc with NIU_BINDKEYS (one key:widget entry per line):
niu_fzf_file() {
local file
file="$(fd -t f | fzf)" || return 0
NIU_WIDGET_RESULT="ni $file"
NIU_WIDGET_ACCEPT=1
}
NIU_BINDKEYS="Ctrl+X:niu_fzf_file"
When the key fires, the function runs with the current editor state:
- In:
NIU_WIDGET_BUFFER(current buffer) andNIU_WIDGET_CURSOR(byte offset). Both are temporary and restored afterwards. - Out:
NIU_WIDGET_RESULTreplaces the buffer (unset or absent keeps it; an empty string clears the line),NIU_WIDGET_CURSOR_RESULTmoves the cursor to a byte offset, andNIU_WIDGET_ACCEPT=1submits the buffer as if Enter had been pressed.
Keys are single-key sequences such as Ctrl+X, Alt+G, a plain character,
or escape forms like ^X. The function runs as ordinary shell code: aliases,
PATH, and your rc all apply. Unknown widget names in bundle bindkeys follow
the same contract, so plugins can ship function widgets too.
Shell-Function Completions
A shell function can provide completions for a command's arguments. Declare
it in ~/.niubashrc with NIU_COMPDEFS (one command:function entry per
line):
niu_git_comp() {
local branches
branches="$(git branch --format='%(refname:short)')" || return 0
NIU_COMP_RESULT="$branches"
}
NIU_COMPDEFS="git:niu_git_comp"
When completing arguments for a registered command, the function runs with:
- In:
NIU_COMP_WORDS(space-joined words, matching bashCOMP_WORDS) andNIU_COMP_CWORD(bashCOMP_CWORDsemantics). Both are temporary. - Out:
NIU_COMP_RESULT, one candidate per line, shapedvalueorvalue<TAB>description.
A NIU_COMPDEFS line can also name a directory (issue #306): every line
that resolves to an existing directory is loaded as a completion-definition
directory — <cmd>.toml files in the same schema as the embedded applet
definitions, and <cmd>.bash standard bash completion scripts (read as
data, never executed). Remaining lines are command:function pairs:
NIU_COMPDEFS="C:/agents/completions"
NIU_COMPDEFS="$NIU_COMPDEFS
git:niu_git_comp"
Compdef functions run synchronously during completion, while the line editor
owns the terminal. They must not print to stdout: compute with command
substitution and write NIU_COMP_RESULT instead. Bundle keybindings TOML and
NIU_BINDKEYS share the same widget vocabulary, so plugins and users can
compose the two contracts freely.
Git Prompt Performance
Git status should be consumed as a coherent prompt snapshot, not rendered by blocking every prompt draw with fresh Git processes. The intended shape is:
- prompt/theme plugins render the latest available snapshot;
- the host keeps git status work warm in the background;
- late git work updates the next prompt instead of repainting the active input line.
If the prompt flickers or repaints the current line, debug the lifecycle and git snapshot path rather than adding more inline Git calls to the theme.
Plugin Workflow
Use the CLI to inspect the ecosystem instead of relying on stale docs:
niu plugin list # sources, their assets, activation state
niu plugin discover # read-only overview, including not-yet-installed managers
Enablement is per source or per asset (a theme), and it edits your rc, not an internal database:
niu plugin enable oh-my-bash # guarded loader block appended to ~/.niubashrc
niu plugin enable agnoster # a theme asset -> sets OSH_THEME in that block
niu plugin disable oh-my-bash # removes the block; the built-in floor returns
niu plugin update|sync|restore|clean are the lockfile-style maintenance
verbs for installed sources.
Third-Party Sources
Beyond the curated catalog (oh-my-bash, bash-it, bash-completion),
niu can install any git repository or local path that follows a known
plugin-manager layout. New sources are untrusted by default: nothing
they ship runs until you explicitly trust them.
niu plugin add https://github.com/someone/oh-my-bash-fork.git
# review the cloned source under ~/.niubash/sources/<id>
niu plugin trust oh-my-bash-fork
niu plugin enable oh-my-bash-fork
niu plugin source remove oh-my-bash-fork # delete tree + registry entry
Notes:
- Sources are cloned to
~/.niubash/sources/<id>and registered in~/.niubash/sources/registry.toml; the registry records the origin, ref, path, and trust state. niu plugin update <id>re-fetches the registered origin;niu plugin rollback <id>returns to the previous version.- An untrusted source never activates: enable refuses until
niu plugin trust <id>passes the review gate. - A git ref can be pinned at add time with
niu plugin add <id> --ref <ref>. NIU_PLUGIN_SOURCES_ROOToverrides the sources root (portable setups and tests).
Command Discovery And WPM
Niubash resolves Unix-style commands through normal Windows PATH. When a
command is missing or comes from the wrong provider, inspect the active
installation:
command -v niubash
command -v winuxcmd.exe
command -v ls
winuxcmd.exe wpm index status
winuxcmd.exe wpm search jq
winuxcmd.exe wpm links rebuild --force
Do not assume /usr/bin exists. Niubash is a Windows process using Windows
executables and command links.
Elevated Commands
Niubash disables Rubash's experimental sudo builtin by default. Windows
elevation is delegated to the WPM gsudo package, which owns UAC, process
creation, environment forwarding, and console handling.
command -v gsudo
gsudo --version
gsudo your-command args
Install or repair it through the active WPM provider when necessary:
wpm search gsudo
wpm install gsudo
wpm links rebuild --force
Do not alias sudo automatically in shared scripts. A user who wants the
Unix spelling interactively can add this to ~/.niubashrc:
alias sudo='gsudo'
The embedded Rubash elevation builtin remains available only for host
integrators. Set NIU_ENABLE_RUBASH_SUDO=1 before starting Niubash, or run
enable sudo in a shell that supports an elevation handler. This is not the
recommended Windows path.
Windows Paths And Home
Prefer durable Windows paths in scripts:
cd C:/Users/you/repo
ls "C:\Program Files"
cd ~
Prompt display should normally render the home directory as ~ and descendants
as ~/path, but internal process paths remain native Windows paths. Treat
/c/Users/... as compatibility input, not the primary model.
Updating
Keep the three update planes separate:
niu --self-update --check
niu --self-update
winuxcmd.exe wpm update winuxcmd
niu plugin update oh-my-bash
niu plugin rollback oh-my-bash
niu --self-updateupdates the shell.wpm update winuxcmdupdates command packages and command links.niu plugin update <source-id>updates an installed plugin source;rollbackreturns it to the previous version.
Debug Checklist
For shell issues, capture the active binary and execution path first:
niu --version
command -v niubash
command -v winuxcmd.exe
echo "$SHELL"
niu -c 'echo command-mode:$SHELL'
niu -C 'echo repl-command:$SHELL'
For repository changes, run focused tests before broad suites:
cargo test --test repl_command --locked
cargo test -p niubash-runtime --lib --locked
cargo test --test plugin_inventory --locked
Use Plugin System Direction for architecture and Plugin System Roadmap for execution order.
Plugins & the Bash Ecosystem
Niubash does not vendor a plugin framework. The plugin system is the external
bash ecosystem as first-class content: oh-my-bash, bash-it and
bash-completion install over git clone, earn activation through an explicit
trust review, and expose their real themes/plugins/aliases/completions through
their own selection mechanisms. Nothing is sourced until you say so.
This page is the user guide; the last section is a compact quick reference for AI agents driving niu on a user's behalf.
The five nouns
| Noun | What it is | Where it lives |
|---|---|---|
| Source | A git tree installed by niu (a manager like oh-my-bash, or any single-purpose repo) | ~/.niubash/sources/<id>/ |
| Asset | A theme/plugin/alias/completion inside a manager source | enabled via the manager's own mechanism |
| Recipe | A data row describing one ecosystem asset and how to install it (~500-row compiled-in index) | niu plugin recipe list |
| Collection | A named manifest of recipe ids — a starting-point bundle (LazyVim-extras pattern) | built-in, or ~/.niubash/distros/ |
One install driver exists, and only one: git (tree sources, via
niu plugin add <git-url>). Niubash carries zero network/HTTP download
responsibility (download retraction, owner ruling 2026-10-04): executable
tools (fzf, starship, …) install through your real package managers —
wpm first on Windows (owner correction 2026-10-03), winget/scoop as
alternatives for what wpm does not carry (GUI apps, fonts), and
apt/dnf/yum/brew on other platforms. The recipe rows for those tools stay
in the index as catalog metadata and add prints the commands.
Everyday flow
$ niu plugin discover # read-only: what's out there
$ niu plugin add oh-my-bash # git-clones the manager (UNTRUSTED)
$ niu plugin trust oh-my-bash # review checksum, flip the execution gate
$ niu plugin list # sources, assets, activation state
$ niu plugin enable agnoster # activate a theme through oh-my-bash
The trust gate is the load-bearing rule: install and activation are separate
verbs. add/apply land content on disk but nothing from it executes in
your shell until niu plugin trust <id> succeeds (checksum review) or you
enable an asset explicitly. Collection applies honor the same rule.
Manager sources (catalog ids like oh-my-bash) are also declared in the
spec ~/.niubash/plugins.toml — the single source of truth for what is
declared; niu plugin sync reconciles the spec with what is installed,
and niu plugin update <id> (no id = all) moves sources to their ref tips.
Imperative mode → adoption (1.3.1)
A machine with installed sources but no spec is in imperative mode: the
startup form stays silent (nothing to reconcile), and
niu plugin sync lists the state with the migration one-liner:
$ niu plugin sync --adopt
declared oh-my-bash (installed; adopted into the spec)
...
--adopt declares every installed-but-undeclared source, snapshotting the
live enablement (enable = [...]) and the active theme (theme = ...)
from the machine's current state, so the adopted spec round-trips — a
plain niu plugin sync afterwards is a no-op. niu plugin add <target>
on an already-installed source declares it (never the old "remove it
first" refusal), and the first-run wizard's collection apply now ends
spec-managed (the run itself adopts, picked theme included). Startup
installs that fail are memoized and not retried on every terminal —
explicit verbs retry.
Undo is printed with every mutating command (disable, source remove,
source rollback) and the setup wizard journals one undo
command per thing it changed.
Recipes: the index
$ niu plugin recipe list # the whole index
$ niu plugin recipe list --category prompt # manager|theme|plugin|alias|completion|prompt
$ niu plugin recipe list --json # machine-readable (agents: prefer this)
$ niu plugin recipe show starship # driver, version, license, state
$ niu plugin add starship # prints package-manager install commands
Rows with driver = git install as tree sources; executable-tool rows
(driver = "download" in the generated index) are catalog metadata —
add prints package-manager install commands (wpm first on Windows,
native managers elsewhere) and never fetches; rows with no driver are
info-only (e.g. ble.sh, which builds from source) and add explains
instead of installing.
Collections: starting points
$ niu plugin distro list # built-in + imported
$ niu plugin distro apply recommended
$ niu plugin distro import <git-repo-or-path> # any repo with niu-collection.toml
$ niu plugin distro remove <name>
Built-ins: minimal (bash-completion only), recommended (oh-my-bash + its
default theme + completions — also offered by the first-run wizard),
full (both frameworks + bash-preexec; its fzf/starship entries print
package-manager suggestions, since niu downloads nothing).
A collection is a small TOML manifest of recipe ids:
schema = "niubash:plugin-collection@1"
name = "my-starter"
description = "what this bundle is"
[[entry]]
recipe = "oh-my-bash"
[[entry]]
recipe = "starship"
Imports are first-class: anything the built-ins can do, your own repo can do,
byte-identically. apply installs every entry through its recipe driver and
never grants trust — you review afterwards. Failures are collected per
entry (a bad entry never kills the rest) and reported with the retry verb.
The first-run wizard's plugin-collection question is a LazyVim-style
progressive-disclosure hook: it only appears when the ecosystem is empty, and
Skip is the default.
When the picked collection installs a theme-bearing source (recommended/
full), the same wizard run immediately offers the theme pick — one run,
out of the box. The gallery lists trusted sources only, so the flow first
asks a single trust question (the wizard's phrasing of
niu plugin trust, same checksum tier); answering it lists the fresh
source's themes, and the pick lands in the rc through the same guarded
block a later niu plugin enable <theme> writes, journaled with its own
undo line. Declining (or Esc) changes nothing — the run prints the exact
niu plugin trust <id> command plus the re-run/enable follow-up instead.
Tools: package-manager recommendations
$ niu plugin add fzf # prints: wpm install fzf (Windows, first choice),
# winget/scoop alternatives, apt/dnf/brew,
# plus the upstream release URL
Nothing is fetched and nothing lands on disk — run the printed command with
your package manager. The retired niu plugin tool verbs fail with a
pointer here; a download-era install under ~/.niubash/tools/<id> can be
removed by deleting the directory.
The menu UI
niu plugin ui opens a menu-level UI (needs an interactive terminal):
sections ordered by what needs attention — untrusted sources first, then
ready sources, installable recipes, collections, global verbs. Every
action in the menu is the same verb the CLI runs (the footer shows the CLI
equivalent of each); there is no UI-only logic. Rows read
[state] id — hint.
For AI agents (quick reference)
When driving niu on a user's machine:
- Prefer
niu plugin recipe list --jsonandniu plugin list [--json]for state; plain-text output is for humans and changes freely. - Never auto-trust.
niu plugin add .../distro apply ...are safe (content lands but does not execute).niu plugin trust <id>flips the execution gate — that is a human review step. Report untrusted ids to the user with the exact command they should run. - Trust requires an intact tree; a degraded source (missing directory) cannot
be trusted — suggest
niu plugin restore <id>. - Mutations print their undo (
disable,source remove,source rollback <id>). Surface those undo verbs to the user. niu plugin update <id>moves a source to its ref tip (no id = every source);niu plugin syncreconciles the spec with reality;niu plugin restorerebuilds from the lockfile pin in~/.niubash/sources/registry.toml.- Exit codes: success
0; usage errors and refused operations (unknown id, builtin-name collision, non-ttyui) exit non-zero with a message that names the object and the repair verb. - Env overrides used by tests/tools:
NIU_PLUGIN_SOURCES_ROOT,NIU_PLUGIN_DISTROS_ROOT.
External Completion Providers
Niubash ships no third-party completion provider in its binary. The shell
provides the protocol; providers are data you source from ~/.niubashrc
— the same setup a bash or zsh user writes by hand. Any tool that speaks the
bash completion protocol integrates this way; carapace-bin below is one
example, not a special case.
The protocol surfaces
Three surfaces accept external completion code, all configured from the startup rc:
1. complete compspecs (the GNU builtin)
The engine implements the complete builtin, so standard completion
registrations work as-is:
complete -W 'status switch merge rebase' mytool # static wordlist
complete -f tar # any file
complete -d cd # directories only
Static compspecs serve candidates at Tab directly from the engine.
Dynamic hooks — complete -F func and complete -C command — register and
display correctly (complete -p), and -F functions execute inside scripts
through compgen -F; their resolution at the interactive Tab press is an
engine follow-up (see the rubash release notes). Until it lands, a
function-based provider should also speak the compdef protocol below.
2. compdef functions (NIU_COMPDEFS)
For providers that compute candidates with a shell function, the rc declares
a command:function mapping and defines the function. The function sees
NIU_COMP_WORDS (space-joined, bash COMP_WORDS semantics) and
NIU_COMP_CWORD; it writes back NIU_COMP_RESULT, one candidate per line,
as value or value<TAB>description — the description shows up in the
completion menu.
_mytool_comp() {
# a real provider would shell out here and translate its output;
# carapace's `value<TAB>description` fish output drops in unchanged
NIU_COMP_RESULT="start"$'\n'"stop"$'\n'"restart\tRestart the service"
}
NIU_COMPDEFS="mytool:_mytool_comp"
Candidates are filtered by the typed prefix; NIU_COMPDEFS holds one
command:function pair per line.
3. Completion definition files
<cmd>.toml (flags, subcommands, value sources — the richest surface) and
<cmd>.bash (standard bash completion scripts: the opts="..." line plus a
case "${prev}" block with compgen value lists) are parsed into the same
definitions the embedded base layers use. The files are never executed: both
formats are read as data.
A completion-definition directory is declared with a directory line in
NIU_COMPDEFS — the same variable that carries command:function pairs
(issue #306). Every line that resolves to an existing directory is loaded
(both file formats); every other line is a command:function pair. A user
mytool.toml overrides the compiled-in definition for mytool entirely:
# ~/.niubashrc — completion files as plain rc data
NIU_COMPDEFS="C:/agents/completions"
NIU_COMPDEFS="$NIU_COMPDEFS
mytool:_mytool_comp"
C:/agents/completions/mytool.toml # command/flags/subcommands TOML
C:/agents/completions/othertool.bash # standard bash completion script
Built-in base layers
Two compiled layers ship in the binary, so a fresh install completes common commands with no configuration at all:
- every WinuxCmd applet (
grep --col<Tab>→--color/--colour,ls --color <Tab>→always/auto/never) — generated from the applet--helptranscripts; - common external commands — git, tar, curl, ssh, docker, kubectl, and npm —
as static subcommand/flag tables (issue #305):
git co<Tab>offerscheckout/commit/config,kubectl get -<Tab>offers-o/--all-namespaces,curl -X <Tab>offers the HTTP methods.
Priority
Local definitions always win over external providers: the embedded applet and common-command definitions, bundle definitions, and user completion files are consulted before any rc-integrated provider fires, so a provider can never shadow a definition you already have. Within the file layers a later definition replaces an earlier one per command: embedded defaults → bundle definitions → user completion files.
Example: carapace-bin
carapace-bin is a single binary carrying community completion specs for ~1200 commands (git, curl, gh, docker, ...). Its bash integration emits standard bash completion code, so the standard snippet is all it takes:
# ~/.niubashrc — requires carapace-bin installed, e.g.
# wpm install carapace-bin
# (or see https://github.com/carapace-sh/carapace-bin/releases).
# Until carapace is on PATH this block is a no-op.
if command -v carapace >/dev/null 2>&1; then
source <(carapace _carapace)
fi
niu setup offers this snippet as an opt-in (default off) — the wizard
writes the same block, it installs nothing.
Static compspecs from the snippet serve candidates today; the snippet's
complete -F hooks light up fully when the engine's interactive -F/-C
resolution lands. Providers that want description-carrying candidates
today can bridge through the compdef protocol — carapace's
carapace <command> fish <words...> output (value<TAB>description, already
prefix-filtered) matches NIU_COMP_RESULT's format line for line:
# compdef bridge: carapace data through niubash's function protocol
_carapace_bridge() {
local cmd
set -- $NIU_COMP_WORDS
cmd=$1
NIU_COMP_RESULT="$(carapace "$cmd" fish $NIU_COMP_WORDS 2>/dev/null)"
}
NIU_COMPDEFS="git:_carapace_bridge"
NIU_COMPDEFS="$NIU_COMPDEFS
docker:_carapace_bridge
curl:_carapace_bridge"
List the commands you care about — NIU_COMPDEFS is plain rc data, edited
any time.
Hooks
Status (2026-10, niubash#161): the niu-native named-hook registry described
by the previous version of this page is not implemented. The
niubash_add_*_hook registration helpers were part of the oh-my-niu
framework, which retired with the built-in plugin/theme stack
(niubash#145); there is currently no runtime registration path — the host
loads an empty hook configuration, so the internal run_*_hooks runners
have nothing to run. Whether (and how) hooks return for the
external-ecosystem world — e.g. registration builtins over the shell's own
function namespace — is an open owner ruling.
What exists today is the bash-native hook surface. External frameworks (oh-my-bash, starship, your own rc) already use it, and it behaves exactly as in GNU bash:
| Lifecycle point | Bash-native mechanism | Notes |
|---|---|---|
| before every prompt | PROMPT_COMMAND (function or string) | runs each prompt cycle, before the prompt is rendered |
| before a command executes | PS0 (expanded, printed) | after you press Enter, before execution |
| prompt identity | PS1 assignment | claims the prompt slot (defaults-as-floor: the product floor never fights an active claim; unset PS1 restores it) |
| directory change | chpwd-style: cd hooks via PROMPT_COMMAND + PWD diffing in your own function | no dedicated chpwd hook yet |
| signals / exit | trap 'handler' INT TERM EXIT DEBUG ERR | full bash trap semantics, via the engine |
| terminal title | write OSC escapes from PROMPT_COMMAND (or set NIU_TITLE) | NIU_TITLE, when set, is the value title logic sees |
Example — the starship shape (a PROMPT_COMMAND hook that claims PS1 in
the same render cycle):
# ~/.niubashrc
starship_precmd() { PS1="$(starship prompt)"; }
PROMPT_COMMAND="starship_precmd${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
eval "$(starship init bash)"
Example — time every command from PROMPT_COMMAND (start-of-command stamp
plus end-of-command report):
# ~/.niubashrc
__niu_t0=$SECONDS
__niu_timer_report() {
echo "last command took $((SECONDS - __niu_t0))s"
__niu_t0=$SECONDS
}
PROMPT_COMMAND="__niu_timer_report${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
Example — PS0 is expanded and printed right after you press Enter, before
execution (same expansion rules as PS1):
PS0='[running...] '
The internal runners (precmd/preexec/title/… cycle calls in
shell.rs) are retained: they carry the live machinery above
(PROMPT_COMMAND execution, PS0 rendering, PS1 claim/release sync,
title resolution) and keep the call sites for the eventual
external-ecosystem hook model. They are intentionally not a user-facing
surface until that model is ruled on.
Installer and Self-Update
Niubash ships two Windows package shapes:
niubash-v<version>-win-<arch>-setup.exefor normal users.niubash-v<version>-win-<arch>.zipfor portable, agent, or scripted use.
The installer is built with Inno Setup and installs per user by default under:
%LOCALAPPDATA%\Programs\Niubash
It does not require administrator privileges. The default installer tasks:
- add the install directory to the user's
PATH; - add or update a Windows Terminal profile named
Niubash; - set that profile's command line to the installed
niu.exe; - set that profile's starting directory to
%USERPROFILE%; - point the Windows Terminal profile icon at the installed PNG asset.
The Windows Terminal profile is installed by running:
niu --install-wt-profile --quiet
Users can run this command again after moving an install. To also set Niubash as the Windows Terminal default profile, run:
niu --install-wt-profile --set-default
Self-update uses Windows WinHTTP directly to follow the GitHub Release
releases/latest redirect, download the latest installer for the current
architecture, and start it silently. It does not depend on the GitHub REST API.
niu --self-update
Inside an interactive Niubash REPL, use:
self-update
The REPL command hands the update to a child process and exits the current
shell so the installer can replace niu.exe.
Useful dry-run modes:
niu --self-update --check
niu --self-update --dry-run
Interactive shells check for updates at most once per day. The check is
best-effort and silent on network failures; when a newer release exists, Niubash
prints a short hint to run self-update in the REPL or
niu --self-update outside it. Set
NIU_UPDATE_CHECK=0 or NIU_NO_UPDATE_CHECK=1 to disable the reminder.
The installer invokes winuxcmd.exe wpm links rebuild --root ... --force after
copying the files, so the bundled commands are materialized immediately. On NTFS,
WPM creates hard links to the installed winuxcmd.exe. The portable zip keeps the
first-start fallback: if command links are missing, Niubash runs
winuxcmd/activate-winuxcmd.sh once from the bundle.
Updating WinuxCmd with WPM
The Unix command set (ls, cat, grep, sed, ...) is delivered by
WinuxCmd and managed separately from the Niubash binary. Winux Package
Manager (wpm) handles it:
wpm update winuxcmd # update WinuxCmd from the local index
wpm index status # inspect the local index state
wpm list # indexed packages and install state
wpm update winuxcmd refreshes the command set in place; command links are
rebuilt automatically. Run wpm --help for the full surface (index,
source, search, info, install, links).
So the update story has two parts:
niu --self-update # the shell itself
wpm update winuxcmd # the Unix commands it ships with
Bash And sh Command Links
bash.exe and sh.exe are the shell itself under another name: where
hardlinks are available they are hardlinks to niu.exe, and the engine takes
the face from argv[0] ($0, BASH_VERSION, and POSIX mode for sh/ash).
That is worth real time - a hardlink costs nothing, while a forwarding launcher
runs one extra process on every bash -c / sh -c (measured at ~13 ms per
call, unixwin/niubash#377).
Run this after laying the tree down, and again after any update that replaces
niu.exe (a replaced binary leaves the names pointing at the previous inode):
niu --relink-shells --force
It links bash.exe and sh.exe in <root>\bin, <root>\winuxcmd\bin and
<root>\winuxcmd\usr\bin, skips directories that are not present, is
idempotent, and parks an existing name out of the way before replacing it, so a
shell that is running right now keeps its bytes. The Niubash installer does
this from [Run].
Where hardlinks are unavailable (exFAT, a network share) the shipped forwarding
shim stays the fallback. It sets NIU_INVOKED_AS=bash / NIU_INVOKED_AS=sh
and re-executes niu, which produces exactly the identity the argv[0] path
provides - keep both working instead of assuming either:
NIU_INVOKED_AS=bash
NIU_INVOKED_AS=sh
NIU_INVOKED_AS=ash
Niubash consumes that value before constructing Rubash; a plain niu.exe
launch must leave it unset, and a launcher must preserve all original argv
values rather than parsing shell options of its own.
Installer acceptance tests for either provider:
bash.exe -c 'test -z "$POSIXLY_CORRECT"'
sh.exe -c 'set -o | grep posix'
ash.exe -c 'set -o | grep posix'
This repository does not create the links inside a WinuxCmd tree by itself; the
installation provider calls niu --relink-shells (or implements the
NIU_INVOKED_AS contract above).
The PATH Entry Directory Contract
Only two directories of the installation ever appear on the process PATH:
{app} itself (so niu resolves) and <root>\winuxcmd\bin — the single
command directory. The contract for what may live in the command directory
(niubash#376):
- Hardlinks to the payload only, never forwarding launchers. A hardlink is
the same file — the spawn cost equals the payload's (
git.exethere measured 12.7 ms against a forwarding wrapper's 32.2 ms, unixwin/niu-git#8). A launcher re-executes the real binary and pays a full process per call, which removes the entire reason the entry exists. usr\binnever goes onPATH. It holds the whole Unix command set; putting it there shadowsgit,cygpath,find,sort,sedand friends in Git for Windows sessions and every GUI process.- The entry set is declared, not accumulated.
wpm links rebuild(installer[Run]) materializes the package-declared links, andniu --relink-shellskeeps the shell faces on the currentniu.exeinode. An undeclared link survives as a stale binary while the payload moves on. - Dead payloads are swept, not kept. Retired shapes —
*.bak-*and*.old-*left by earlier updaters, and*.niu-old-*, the relinker's park name when a crash lands between park and drop — are removed by the installer's[InstallDelete]/[UninstallDelete]sweeps; a directory onPATHmust stay auditable. The sweeps match only these generated shapes inside the two app-owned directories; a user file that borrows one of these suffixes there would be removed with them, so keep user files out of the installation tree.
Accepted limitation: in a GUI-launched process bash still resolves to the
WSL launcher (System32 and WindowsApps sit ahead of the user PATH);
sh and git resolve to the entry directory. Winning the bash name would
need a machine-scope PATH entry placed before System32, which a per-user
install does not do.
Plugin Ecosystem In Packages
Release packages ship no plugin content. The built-in plugin/theme stack —
including the oh-my-niu bundle — is retired (niubash#145), and the plugin
verbs hard-bail on bundles, so staging one would lay down a tree the product
refuses to use (niubash#161). Packages contain the shell, the WinuxCmd
command set, and the command links.
The external bash ecosystem (oh-my-bash, bash-it, bash-completion, wild
plugins) installs on explicit user command — niu plugin add <id> clones
from upstream into the user profile, under the trust protocol. There is
nothing to pre-stage and nothing to update with the shell.
Rubash Bash Compatibility Matrix
This matrix keeps the boundary clear: rubash owns shell language semantics, while Niubash owns Windows host integration, REPL behavior, completion, plugin routing, and winuxcmd command discovery. Use it when deciding whether a fix belongs in rubash or in the Niubash host layer.
Verification Layers
| Layer | Scope | Current evidence |
|---|---|---|
| Local compat fixtures | Focused Niubash binary tests for common bash semantics that depend on rubash plus winuxcmd command links. | CARGO_TARGET_DIR=target/codex-verify-phase17 cargo test --test compat --locked -- --ignored passed 18/18 on 2026-07-31. |
| Host contract tests | Windows process, cwd, stdin, script, env, and stdio behavior around rubash execution. | Covered by tests/host_contract.rs; full workspace test passed in the Phase 16 verification run. |
| GNU Bash upstream local gate | Broader upstream bash fixture from a sibling rubash checkout, intentionally local-only and not vendored. | docs/planning/bash-upstream-local.md records the Niubash local gate; the current rubash upstream ledger is the rubash README "Compatibility at a Glance" section: 83 suites, 82 pass, 1 residual (nameref), re-run 2026-09-25 on rubash master 71c933eb. |
Focused Compat Fixtures
| Capability | Evidence fixture(s) | Status | Boundary |
|---|---|---|---|
| Variables and simple parameter expansion | var_expansion, string_param | Passing | rubash parser/executor. |
| Command substitution | command_substitution, command_substitution_quoted_newline, command_substitution_function_pipeline | Passing | rubash command substitution; host still owns -c quoting and process invocation. |
| Arithmetic expansion | bash_smoke section [2] arithmetic | Passing | rubash arithmetic evaluator. |
| Indexed arrays | bash_smoke sections [3] arrays, [16] array slice | Passing | rubash arrays and parameter expansion. |
| Associative arrays | bash_smoke section [4] assoc arrays | Passing | rubash declare -A and associative lookup. |
| Boolean list status | and_or_status, bash_smoke section [20] exit status | Passing | rubash &&, ` |
| If / elif / else | if_else, multiline_if, bash_smoke section [11] if | Passing | rubash compound commands; Niubash must feed full scripts to rubash. |
| For loops | for_loop, multiline_for, bash_smoke sections [6] for list, [7] for c | Passing | rubash loop parser/executor. |
| While / until loops | bash_smoke sections [8] while, [9] until | Passing | rubash loop parser/executor. |
| Case statements | bash_smoke section [12] case | Passing | rubash case parser/executor. |
| Functions | function, command_substitution_function_pipeline, bash_smoke section [10] function | Passing | rubash function definition and invocation. |
| Aliases | alias | Passing | Niubash installs aliases into rubash; expansion is rubash-owned. |
| Pipelines | pipeline, command_substitution_function_pipeline, bash_smoke section [13] pipeline | Passing | rubash pipeline execution plus winuxcmd command links. |
| Redirection | bash_smoke section [14] redirect | Passing | rubash redirection with Niubash/winuxcmd filesystem behavior. |
| Heredocs | heredoc | Passing | rubash whole-script parsing; host stdin/script path must avoid line-by-line splitting. |
| Backslash continuations | continuation | Passing | rubash whole-script parsing. |
| Echo flags | echo_flags | Passing | shell builtin behavior as exposed through rubash/winuxsh. |
| Export to Windows child process | bash_smoke section [19] export | Passing | rubash environment plus Niubash process environment synchronization. |
| File tests | bash_smoke section [18] file tests | Passing | rubash test builtin plus host filesystem paths. |
Host Contract Coverage
| Host surface | Evidence | Notes |
|---|---|---|
| cwd authority | cwd_cd_pwd_and_windows_child_process_agree, drive_only_cd_and_bare_drive_commands_switch_to_drive_root | Niubash normalizes/synchronizes shell PWD with Windows child process cwd. |
| startup isolation | winshrc_does_not_run_for_non_interactive_modes | Non-interactive -c, script file, and stdin script paths do not source REPL startup rc. |
| temporary assignments | temporary_assignment_reaches_nested_winuxsh_child | Assignment semantics are observable by nested Niubash child processes. |
| stdin scripts | piped_stdin_without_args_runs_plain_script_surface, piped_stdin_without_args_runs_multiline_compound_block, piped_stdin_without_args_runs_heredoc_as_one_chunk | Host feeds complete stdin scripts to rubash for multiline/heredoc semantics. |
| script positional parameters | script_file_args_populate_positional_parameters | Host script path preserves $0/positional parameter behavior. |
| Windows child env | exported_env_reaches_windows_child_processes, sourced_rc_keeps_winuxcmd_visible_to_windows_children | Niubash bridges rubash env changes into Windows child process launches. |
| stdio and exit code | stdout_stderr_and_exit_code_are_preserved, closed_stdout_pipe_does_not_print_broken_pipe_error | Host preserves process surfaces expected by agents. |
| command-mode parsing edge cases | command_mode_accepts_base_prefixed_arithmetic_in_function_body, command_mode_parameter_pattern_removal_handles_escaped_quotes, command_mode_set_positional_splits_custom_ifs | Focused regressions for rubash-facing -c script delivery. |
Known Gaps and Routing
| Gap | Route |
|---|---|
| Full GNU Bash upstream gate is local-only and not normal CI. | Keep using docs/planning/bash-upstream-local.md for the Niubash local gate; source rubash suite numbers from the rubash README compatibility ledger. Do not vendor upstream bash tests. |
niu -c still has host-side rough edges around POSIX assignment prefixes, env VAR=value cmd, heredoc temp-file flows, and complex quoting in agent commands. | Track as Niubash command-mode/host issues, not as rubash language failures unless a direct rubash fixture reproduces it. |
| Job control and interactive terminal process-group semantics are not covered by the focused compat matrix. | Route through rubash first; add Niubash host tests only for Windows process integration. |
| Plugin runtime behavior is intentionally outside current shell compatibility scope. | Keep in plugin docs; do not mix with bash language compatibility claims. |
Maintenance Rules
- Add one focused fixture under
tests/compat/fixtures/before claiming a new bash-language capability in README or roadmap. - Prefer host contract tests for Windows cwd/env/stdin/stdout issues that happen around rubash rather than inside rubash.
- Re-run the ignored compat suite before updating this matrix:
CARGO_TARGET_DIR=target/codex-verify-phase17 cargo test --test compat --locked -- --ignored. - Use the upstream local gate only when parser/executor behavior changes or when syncing a new rubash revision.
Built-ins & Fast Paths
Niubash resolves a command word through three layers, in priority order. This page documents what lives in each layer, how the inventory is kept honest, and where the inventory overlaps with WinuxCmd.
| Layer | What it is | Introspection (type, command -v, enable) |
|---|---|---|
| Real built-in | Runs in-process, no child process. Listed in the rubash whitelist (src/executor/builtin_names.rs). | Reports as builtin |
| Fast path | Hidden optimization: runs in-process only for simple argument shapes; falls back to the external WinuxCmd command otherwise. Deliberately not in the whitelist. | Reports as external — same as GNU Bash, where these commands are external too |
| External command | Resolved via PATH, provided by WinuxCmd (176 commands). | Reports as external |
Real built-ins (64)
All 61 GNU Bash 5.2 built-ins are covered — none missing. The entries below that are not part of Bash are deliberate extensions:
| Group | Commands |
|---|---|
| GNU Bash 61 built-ins (all aligned) | . : [ alias bg bind break builtin caller cd command compgen complete compopt continue declare dirs disown echo enable eval exec exit export false fc fg getopts hash help history jobs kill let local logout mapfile popd printf pushd pwd read readarray readonly return set shift shopt source suspend test times trap true type typeset ulimit umask unalias unset wait |
| zsh compatibility extension | setopt unsetopt |
In-process env | env (coreutils semantics, sorted output, -0) |
| Windows-only | sudo (not counted in the 64) |
Notes:
declare/typesetandsource/.are alias pairs;[is implemented by thetestbuiltin and validates the closing].time,[[, and((are reserved words / grammar, not built-ins — same as Bash.timehonorsTIMEFORMAT.- The
sudobuilt-in is a Windows-specific extension (#[cfg(windows)]).
Fast paths (hidden built-ins)
These run in-process for simple argument shapes and fall back to the external WinuxCmd command otherwise. They are intentionally excluded from the whitelist so that introspection keeps reporting them as external, matching GNU Bash:
| Command | In-process behavior | Fallback |
|---|---|---|
sleep | Fractional seconds supported | external sleep |
dirname | Plain paths | external dirname |
basename | Plain paths | external basename |
env is the one hybrid: it is on the whitelist (reported as a built-in),
but its implementation is fully in-process with coreutils semantics and no
external fallback path.
Two additional reproduction built-ins exist for test compatibility only:
recho and zecho (mirrors of the helper built-ins in GNU Bash's own test
suite; not shipped in official Bash builds).
Overlap with WinuxCmd
Thirteen command names exist both as rubash built-ins/reserved words and as
WinuxCmd external commands: echo env kill printf pwd test [
true false (real built-ins), sleep dirname basename (fast paths),
and time (reserved word).
The overlap follows Bash semantics: inside a script the built-in or reserved
word shadows the external command, while explicit forms such as
command env or /usr/bin/env still reach the WinuxCmd external version.
Both implementations were compared option-by-option (escapes, format
specifiers, signals, jobspec, TIMEFORMAT) on 2026-09-08 — aligned.
How the inventory stays honest
The authoritative machine-readable lists live in
docs/builtins.md on unixwin/rubash.
A doc-sync test inside src/executor/builtin_names.rs parses that document
and fails cargo test whenever the whitelist or the fast-path dispatch
changes without the document being updated — and vice versa. Adding,
removing, or reclassifying a built-in therefore requires touching both the
code and the doc in the same change.
Niubash v2 Architecture
A Windows-native, Bash-compatible terminal built on rubash + winuxcmd
Positioning
Niubash is a Windows-native, non-isolated, Bash-compatible terminal for both humans and agents. It does not implement the shell language itself — it is the interactive front end to the rubash lib (the bash-compatible engine) plus the routing layer onto winuxcmd (coreutils). Its core value is the Windows-native process/environment experience: a reedline REPL, the completion system, the prompt floor, Ctrl+C handling, terminal integration, and a stable non-interactive agent execution contract.
Niubash is not an MSYS2, Git Bash, Cygwin, or WSL-style isolated
environment. ~ points at the normal Windows user home (USERPROFILE /
dirs::home_dir()); PATH, cwd, env, stdout, stderr, and exit codes are
ordinary Windows process state.
Three-layer architecture
niu.exe
├── Niubash host layer (Rust)
│ ├── rubash::Executor ← shell language engine (lexer/parser/execution/builtins)
│ ├── reedline REPL ← line editing, history, completion
│ ├── completion/ ← shell definitions + bash auto-import + 3-level cache
│ ├── prompt/ ← prompt floor (defaults-as-floor, §14.5 of the ecosystem design)
│ ├── config ← legacy/managed machine-state reads
│ ├── plugins ← external bash-ecosystem sources (add/trust/enable, no bundles)
│ └── ctrl_c ← Win32 Ctrl+C handling
├── rubash lib (Rust)
│ ├── lexer/parser/ast
│ ├── executor (pipeline/redirect/alias/function/array/job)
│ └── builtins (cd/source/export/set/test/printf...)
└── winuxcmd.exe (C++) ← Unix coreutils (ls/cat/grep/find/cp/mv...)
Key design decisions
1. rubash as a lib dependency
Niubash links rubash directly as a Rust crate dependency:
[dependencies]
rubash = { git = "https://github.com/unixwin/rubash.git", branch = "master" }
All shell semantics (parsing, execution, built-ins, variable expansion, redirection, pipelines, job control) are delegated to rubash. Niubash does not re-implement lexer/parser/ast/builtins.
2. WinuxCmd is selected by Niubash and integrated via PATH
Not via FFI/DLL — the rubash Executor still finds external commands through
PATH. Version selection is a Niubash session/config responsibility. At
startup:
- Read the explicit
WINUXCMD_PATH, then fall back to Niubash's own install/bundle/PATHdiscovery rules to locate awinuxcmd.exe. - Prepend the directory of that same exe to the process
PATHso that command links such asls/cat/grepresolve. - Pass the exact resolved exe path to rubash via
Executor::set_winuxcmd_path.
Rubash never guesses a different winuxcmd.exe from PATH. This way, stale
command links from an old bundle that are still on the Windows PATH can
never mix the dispatcher with the wrong links.
3. Windows real installation tree
Niubash derives one shell root from the selected installed
winuxcmd.exe. For example, the executable
<install>/usr/bin/winuxcmd.exe makes <install> the root. Niubash creates
the ordinary directories below that root:
<install>/usr/bin
<install>/bin
<install>/usr/local/bin
<install>/etc
<install>/var
<install>/tmp
<install>/dev
<install>/.wpm
usr/bin is canonical for WinuxCmd, WPM, command links, and filename-only WPM
targets. Explicit package targets keep their requested real directory.
Niubash passes the selected installation root to Rubash through
NIU_ROOT; there is no second ~/.niubash/root tree and no provider
union. Rubash maps /, /bin, /usr/bin, /etc, and /tmp directly below
the real root. /dev/null maps to Windows NUL; other /dev entries remain
unsupported capabilities.
4. Completion is independent of the engine
The completion system (shell definitions + bash script auto-import +
cmd -h description scraping + a 3-level cache) is implemented on the
Niubash side and does not depend on rubash. It is one of Niubash's core
differentiators.
5. Configuration and startup entry points
~/.niubashrcis the primary interactive entry point. Plain niubash/bash syntax declares plugin lists, theme, prompt template,export,alias, functions, and local startup logic.~/.winshrcis the compatibility fallback; it is read as the legacy user rc only when~/.niubashrcdoes not exist.- Machine state — plugin CLI enable/disable records, permissions, source registry/trust state, legacy managed blocks, test isolation, completion directories — is maintained by the internal managed-state mechanism and is not a user configuration entry point.
- When
~/.niubashrcexists, it is the single entry point for sourcing plugins/frameworks; the host no longer silently sources the official source plugins a second time from managed-state defaults, avoiding double entry points and duplicate prompt/git-state refreshes. - Plain
niu -c, script files, and stdin scripts stay quiet and deterministic: no interactive rc, no source plugins.
The design principle is to minimize user-visible entry points: users edit
~/.niubashrc day to day; machine state is maintained by Niubash itself and
users never need to edit its storage format.
6. Plugin system
The built-in plugin/theme stack (and the oh-my-niu bundle format) is
retired (niubash#145): the plugin verbs hard-bail on bundles, and release
packages stage no plugin content (niubash#161). The plugin system is the
external bash ecosystem as first-class content.
- Sources are real upstream projects — oh-my-bash, bash-it,
bash-completion, any wild plugin repo or single file — installed over
git clone on explicit user command (
niu plugin add), never vendored into the product. - Sources land untrusted and activate only through the graded trust
protocol (
niu plugin trust); framework assets enable through each manager's own selection mechanism (rc arrays,enabled/entries), never through shims that re-implement upstream library functions. - The declarative spec
~/.niubash/plugins.tomlis the source of truth for what is declared; the registry pins installed trees (commit + checksum), andniu plugin syncreconciles the two without ever granting trust automatically. - Plugins cannot extend the rubash parser/executor, and cannot source
arbitrary legacy
.winshfiles or rc fragments found in user directories beyond what the enabled sources themselves declare. - Editor capabilities come from reedline and Niubash-native keybinding presets.
Repository layout
niubash/
├── Cargo.toml
├── LICENSE # GPL-3.0-or-later
├── README.md / README-zh.md
├── .niubashrc # primary interactive user entry
├── .winshrc # legacy fallback rc
├── managed state # internal machine-managed state
├── crates/
│ └── niubash-runtime/
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs # library entry
│ ├── shell.rs # shell state
│ ├── repl.rs # reedline REPL
│ ├── ctrl_c.rs # Win32 Ctrl+C
│ ├── config.rs # config parsing
│ ├── winuxcmd.rs # winuxcmd discovery
│ ├── prompt.rs # prompt rendering (floor)
│ ├── prompt_segments.rs # segment presets
│ └── completion/ # completion system
├── src/
│ └── main.rs # binary entry
└── docs/
├── src/
│ └── (this documentation book)
└── planning/
Data flow
user types "ls -la | grep foo"
│
▼
reedline (line editing + completion)
│
▼
shell.execute_line(line) ← niubash-runtime
│
├─ rubash::lexer::tokenize(line)
├─ rubash::parser::parse(tokens) → Ast
└─ executor.execute_ast(&ast) ← rubash owns all semantics
│
├─ built-ins (cd/source/echo...)
├─ external command → find_user_command("ls")
│ │ (PATH already carries the winuxcmd dir)
│ ▼
│ winuxcmd.exe ls -la
│
├─ pipeline: | grep foo → find_user_command("grep")
└─ output to stdout
Differences from the old architecture
| Aspect | v1 (old niubash) | v2 (current niubash) |
|---|---|---|
| Shell engine | in-house winsh lexer/parser/ast | rubash lib |
| Coreutils | winuxcmd FFI (DLL, disabled) | winuxcmd.exe process (PATH injection) |
| Command routing | command_router.rs classification table | rubash-internal find_user_command |
| Built-ins | self-implemented builtins.rs | rubash::builtins |
| Completion | src/completion/ | fully preserved, migrated |
| Themes | theme.rs (8 themes) | built-in stack retired (niubash#145); themes come from external sources |
| Plugins | Plugin trait + Oh-My-Niubash | external bash ecosystem as sources (add/trust/enable; no bundles) |
| License | MIT | GPL-3.0-or-later |
Version planning
- v2.2: stabilize the rubash rewrite, completion enhancements, Vi mode and Ctrl+R, configuration consistency, user themes
- v2.3: Windows-native terminal contract, agent-friendly non-interactive behavior, history/prompt/completion UX
- v2.4: interactive polish (right prompt, hints, completion menu, defaults)
- v3: the external-ecosystem plugin system — first-party packs replaced by
real upstream sources (oh-my-bash, bash-it, bash-completion, wild
plugins) under the trust protocol; the
oh-my-niubundle format was retired along the way (niubash#145/#161) - Non-goals: Linux/macOS native shell products; rubash itself is reusable across platforms, but the niubash product targets Windows
Last updated: 2026-09-08
Windows Path Contract
Niubash uses a real Windows directory tree for its Unix-shaped shell paths. This is path spelling support, not MSYS, WSL, Cygwin, a POSIX runtime, or a filesystem overlay.
Ownership
| Layer | Responsibility |
|---|---|
| Niubash | Select one winuxcmd.exe, derive its installation root, create the real tree, and configure the shell session. |
| Rubash | Interpret /, /bin, /usr/bin, /etc, /tmp, cd, source, glob, redirects, tests, and command lookup. |
| WinuxCmd | Implement external commands and native Windows filesystem, process, handle, and device operations. |
| WPM | Install package payloads and command links inside the selected installation root. |
Installation Root
For an installed executable such as:
C:/Users/Administrator/AppData/Local/Programs/Niubash/winuxcmd/usr/bin/winuxcmd.exe
the shell root is:
C:/Users/Administrator/AppData/Local/Programs/Niubash/winuxcmd/
usr/bin/
bin/
usr/local/bin/
etc/
var/
dev/
.wpm/
These are ordinary Windows directories. usr/bin is canonical for
winuxcmd.exe, wpm.exe, command links, and filename-only WPM targets.
Explicit WPM targets under bin, usr/bin, or usr/local/bin remain in that
exact directory. .wpm is private package state and is never a command path.
Niubash passes this exact root to Rubash as NIU_ROOT. Rubash maps paths
lexically below it:
/ -> <root>
/usr/bin/tool -> <root>/usr/bin/tool
/bin/tool -> <root>/bin/tool
/etc/config -> <root>/etc/config
/tmp is deliberately NOT part of this tree: it is a per-user temporary
namespace resolved to the real Windows temp directory (TMPDIR, else
%TEMP%), so it stays writable under any install location
(unixwin/niubash#94). /var/tmp resolves beside it as
<real temp>/var/tmp. For every child that is not a bundled WinuxCmd applet,
external command arguments are translated by the Rubash executor with the same
rules — including /mnt/X drive spellings — so there is
exactly one path-semantics owner and no host-side AST rewrite. Device spellings
such as /dev/null are not part of that set; as arguments they reach the child
verbatim. One child class is exempt from the drive-form rewrite, described under
Shell Namespace Ownership.
Command lookup and native child PATH use these real directories directly.
Rubash does not merge a second provider directory, and WinuxCmd coreutils do
not inspect Niubash variables. Existing flat installations remain usable when
their directory is explicitly present on PATH; new installs use the tree
above.
Shell Namespace Ownership
A child process either resolves the shell namespace itself or receives native paths — never both, and never a guess about the child's capabilities:
- Bundled WinuxCmd applets own the shell tree (unixwin/niubash#346). The
applets selected by the dispatcher route, the configured root, and the
winuxcmd.exetree marker receive/,/usr/bin,/etc,/tmp,/home,/mnt/X, the bare/, and/dev/*VERBATIM, and resolve them through WinuxCmd's own shell-tree layer, which mirrors Rubash's root map. They therefore render the POSIX face (ls -d /usr/bin,realpath /etc,readlink -f /tmp) and stay consistent with the spelling-preservingpwd. The executable-leaf convention is theirs too:/usr/bin/seqresolves to the realseq.exewhilels -dkeeps the operand spelling, sohead -1 /usr/bin/seqandtest -f /usr/bin/seqagree (niubash#177). - Every other child — a native Windows program, or a foreign POSIX runtime
such as MSYS2 or Cygwin — receives native Windows paths. There is
no per-child capability probe and no
MSYS_NO_PATHCONV-style switch; a foreign runtime is treated exactly like a native program. - One exception, and it is not a capability probe:
cmd.exe. A single-letter drive form such as/d/repoor/c/Usersreachescmd.exeverbatim, because/dand/care indistinguishable fromcmd's own switches. Root forms and/mnt/Xspellings are still translated, socmd /c echo /usr/binandcmd /c echo /mnt/c/Usersarrive native whilecmd /c echo /d/repodoes not. The exemption is pinned in Rubash (is_windows_command_processor, and thewindows_cmd_switches_are_not_mapped_into_shell_roottest) and applies tocmd.exealone: a native program that is notcmdreceives/d/repoasD:\repo.
The boundary is structural rather than a rename of the "MSYS NO CONV" problem: the shell never asks a child what it can understand. The shipped toolchain's own resolution decides, and the engine compensates only for the child classes that cannot resolve the tree.
Dispatcher Selection
WINUXCMD_PATH selects one exact dispatcher executable for the session.
Niubash resolves it, prepends that installation's usr/local/bin, usr/bin,
and bin directories to the native PATH, and passes the exact executable to
Rubash. Rubash does not discover another dispatcher from PATH.
The dispatcher is only a fallback when a command is absent from the real tree.
It must not implement Rubash builtins such as cd, export, set, read,
jobs, or trap.
Special Paths
~ is USERPROFILE, the same directory used by PowerShell. Windows paths
such as C:/work/file and /c/work/file remain host paths.
The only device spelling currently supported is /dev/null. As a redirect
target it is mapped to the native NUL endpoint; as an external-command
argument it reaches the child verbatim, like the other /dev entries, which do
not become ordinary files until their fd or terminal capability is implemented.
tags: [niubash, platforms, unix, harmonyos] created: 2026-10-09 status: active
Platform Support
Which platforms Niubash runs on, what "runs" means for each, and the parts of the surface that are deliberately different per platform. This page is the contract; if a platform behaves differently from a claim here, that is a bug.
Status matrix
| Platform | Build | Smoke-gated | Released | Notes |
|---|---|---|---|---|
| Windows x64 | ✅ windows-2025 | ✅ release gate | ✅ .zip + .exe | primary target |
| Windows arm64 | ✅ windows-2025 | ✅ release gate | ✅ | |
| Linux x86_64 (glibc) | ✅ ubuntu-22.04 | ✅ release gate | ✅ .tar.gz | glibc ≥ 2.35 |
| Linux aarch64 (glibc) | ✅ ubuntu-22.04-arm | ✅ release gate | ✅ | native arm64 runner |
| macOS aarch64 | ✅ macos-latest | ✅ release gate | ✅ | |
| macOS x86_64 | ✅ macos-latest | ✅ (Rosetta 2) | ✅ | Apple cross-compile |
| Linux x86_64 (musl) | ✅ ubuntu-latest | ✅ cross-target-smoke | ⬜ | static; see below |
Android (aarch64-linux-android, armv7-linux-androideabi) | ✅ release build-android | ⬜ | ⬜ | incubating: NDK-linked zip, no runtime smoke; see below |
HarmonyOS PC (aarch64-linux-ohos) | ✅ ubuntu-latest check | ⬜ | ⬜ | see below |
"Smoke-gated" means the artifact is built, then run on its own OS, against
the four checks in release.md before it is published —
--version banner, -c execution, a pipeline through native coreutils,
and the offline plugin stack. No smoke, no artifact.
musl
Compiled by cross-target-check and smoke-tested by
cross-target-smoke, which builds the static musl binary and runs the same
four checks natively (a static musl x86_64 binary runs on the glibc
runner). The job asserts ldd reports statically linked first, so a
silent re-link against the runner's glibc cannot pass as a musl build. No
musl-tools package is needed: rustc links against the toolchain's own
self-contained musl crt/libc through the host cc.
No musl release artifact ships yet. That is a delivery decision, not a
coverage gap — flip it on by adding x86_64-unknown-linux-musl to the
build-linux matrix in release.yml.
Android
The build-android legs in release.yml build aarch64-linux-android and
armv7-linux-androideabi with a real bionic link through the NDK
(nttld/setup-ndk, API 24 clang drivers). The bionic cfg surface was fixed
upstream in rubash#456 (AT_EACCESS, confstr, sa_restorer, 32-bit
off_t/S_IFMT widths). The legs are incubating: continue-on-error, not
in the release needs, artifacts go to the workflow run only. The runner
cannot execute bionic binaries, so the four-check smoke gate has not run
for these — a zip from this leg is build-verified, not smoke-verified.
HarmonyOS
The Rust target is aarch64-unknown-linux-ohos. Two facts make it mostly
free:
rustc --print cfg --target aarch64-unknown-linux-ohosreportstarget_os = "linux"andtarget_family = "unix". So Niubash's existing unix leg and rubash's existing Linux leg apply nearly unchanged, andfd/mod.rs's#[cfg(unix)]arm is taken as-is.- The target is Tier 2 with host tools in Rust, so a check-only build needs no external OpenHarmony SDK or clang wrapper.
The one compile blocker found so far was rubash's ulimit, which typed its
resource selector as libc::__rlimit_resource_t — a typedef only glibc
defines. musl and the OpenHarmony libc take a plain c_int. Fixed by
aliasing on target_env (rubash#449). With that, the whole Niubash
workspace compiles for aarch64-unknown-linux-ohos.
cross-target-check (in ci.yml) keeps it compiling. It is check-only
because there is no HarmonyOS runtime available on GitHub runners, so the
gate is "it compiles", not "it runs" — the ohos column of the status matrix
stays ⬜ until a real device runs the smoke gate.
What HarmonyOS PC actually ships
An earlier revision of this page described HarmonyOS as "a de-GNU'd Linux
distribution — musl libc, mksh as the default shell, toybox built in".
That is wrong for HarmonyOS PC. It came from OpenHarmony's dev-board
userland and was applied to the whole platform. The two are different
products and the difference changes how much Niubash has to supply.
HarmonyOS PC is not stripped down. A measured install (HarmonyOS
1.12.0, aarch64, kernel HongMeng Kernel 1.12.0) runs bash as the
shell (/data/service/hnp/bin/bash) and carries 454 commands under
/data/service/hnp/bin — about 93% of a standard Linux command
surface, and 100% of text processing:
grep sed awk cut sort uniq diff diff3 comm cmp head tail wc fold column
Also present: clang/gcc (multi-arch: aarch64 / armv7 / loongarch64 /
x86_64), cmake, make, ninja, autoconf, bison, flex, gdb,
lldb, git (+ git-lfs, git-svn), python3.12, node, full JDK,
ruby, vim/nvim/nano. What is genuinely missing is system
management, not text processing:
| Missing / restricted | Note |
|---|---|
apt / yum / dnf | no classic package manager; use hnpcli or preinstalled |
systemctl | no systemd; HarmonyOS has its own service management |
iptables / nftables | restricted |
docker | not preinstalled |
snap / flatpak | unsupported |
ifconfig / netstat | present but partly restricted (ip, ss preferred) |
The HarmonyOS bash is stock GNU bash
It is worth pinning down what that bash is, because "it has bash" could mean a reimplementation. It is not. It is upstream GNU bash, cross-compiled against musl:
bash --versionon a HarmonyOS PC printsGNU bash, version 5.1.16(1)-release— the GPL banner is GNU's own.- The public HNP build (Termony,
build-hnp/bash/Makefile) pulls$(GNU_MIRROR)/gnu/bash/bash-5.3.tar.gzand configures it with--host aarch64-unknown-linux-musl --without-bash-malloc --disable-nls.--without-bash-mallocmatters: it drops bash's bundled allocator so the musl allocator is used instead. - The build emits
libsh.a,libhistory.aand theloadables(recho,printf,basename, …) — artifacts unique to the GNU bash source tree.
No Huawei-specific shell patches. It is an ordinary musl-linked GNU
bash, which is a useful data point for us: the userland under HarmonyOS is
a musl + Linux-ABI stack, not a bespoke one. That is why Niubash's
target_os = "linux" cfgs all hit and why the port was two lines.
Note the version split when quoting numbers: the measured machine ran 5.1.16, while the public build recipe fetches 5.3. These are different devices at different times — do not present either as "the" HarmonyOS bash version.
Two userland caveats the existing ports hit
Both are worth knowing before a first device run, because neither is a documentation problem — they are things that actually broke people.
1. A third-party port still needed to bundle a shell toolset.
Termony (a "Termux for HarmonyOS PC") ships 42 packages, and its list is
aria2 bash binutils *busybox* ... gcc gdb gettext git ... — bash yes,
coreutils no, busybox instead. So even with 454 commands present system-
wide, an independent porter concluded the stock surface was not enough and
vendored busybox to fill it. That is not proof the 93% number is wrong;
it is evidence that "the tools exist" and "the tools are dependable" are
different claims — the same distinction the argument below turns on.
2. Prefix mismatch inside the built-in Terminal. Termony's README warns:
you can use these utilities in the builtin Terminal app under
/data/service/hnp: Although some paths might get wrong due to prefix set to/data/app/base.org/base_1.0… You can override them like:LD_LIBRARY_PATH=/data/service/hnp/base.org/base_1.0/lib TERMINFO=/data/service/hnp/base.org/base_1.0/share/terminfo fish
This does not contradict the PATH finding below — PATH resolution and
runtime prefix resolution are separate mechanisms. It does mean that a
first device run should check more than "is it on PATH": also confirm that
a resolved binary can actually find its own lib and data dirs. On
HarmonyOS 6.0+ the README adds that sudo is available and system
environment variables are editable, which is how the overrides are meant
to be persisted.
The OpenHarmony dev boards are the limited case
There the userland is toybox, and Huawei's own documentation says it is
trimmed per device:
In the current version, different devices support different toybox commands. You can run the
toyboxcommand to obtain the full list.
Community reports go further: toybox: Unknown command diff even though
diff.c exists in third_party/toybox/toys/pending and is listed in
BUILD.gn. The workaround people use is "选择使用 busybox 代替 toybox".
The third-party pkgsrc bootstrap lists gawk and grep as required
dependencies, which implies they are absent by default.
The honest split:
| HarmonyOS PC | OpenHarmony dev boards | |
|---|---|---|
| Shell | bash | mksh |
| Command set | 454 commands at /data/service/hnp/bin (~93%) | toybox, per-device trimmed |
| Text processing | fully covered | gaps; awk/grep absent, diff may be "unknown" though the source exists |
| What Niubash adds | a consistent bash + GNU-toolset baseline across OS versions, plus the Bash semantics | the missing tools, not just the semantics |
State the argument plainly, because it changed: on HarmonyOS PC the value is not "the commands are missing". It is that the toolset is not guaranteed uniform across device revisions, and that a bash-compatible runtime is still what you want when you do not control the host. Both are real, but weaker than "there is nothing here", and this page should not overstate them.
Packaging: HarmonyOS PC requires HNP
HarmonyOS PC does not accept the tarball layout the Linux legs ship. The constraint is documented for native ports and it is absolute:
在鸿蒙PC上,由于系统安全规格限制等原因,暂不支持通过"解压 + 配 PATH"的方式直接使用 tar.gz 包 ❌ 不能直接解压 tar.gz 包到任意目录 ❌ 不能通过设置 PATH 环境变量来使用 ✅ 必须打包成 HNP(HarmonyOS Native Package)格式才能正常使用
HNP = HarmonyOS Native Package, the platform's native package format —
the analogue of MSI on Windows or deb/rpm on Linux. Packed with hnpcli
(which ships in the OpenHarmony SDK):
hnpcli pack -i <pkg-dir> [-o <out-dir>] # with hnp.json present
hnpcli pack -i <pkg-dir> -o <out-dir> -n <name> -v <ver> # without
The payload is a directory with bin/, optional cfg/ and lib/, and a
required hnp.json:
{
"type": "hnp-config",
"name": "hnpsample",
"version": "1.1",
"install": {
"links": [
{ "source": "/bin/hnpsample", "target": "hnpsample" }
]
}
}
source is relative to the package root and target becomes a symlink
name in the bin directory. If links is omitted, every binary under
bin/ gets a symlink by default. Constraints: no spaces or special
characters in name/version, no non-ASCII directory names, package
≤ 4 GB. Binaries that need their own libraries should carry an rpath
(-Wl,-rpath=${ORIGIN}/../lib).
The distribution path is the part that matters. An .hnp is not
installed standalone. It is embedded in a HAP — an app bundle —
declared in module.json5 as hnpPackages with "type": "public" or
"private", and the HAP is signed and distributed through AppGallery:
HAP project root
└── hnp/ # hnp root, as passed to --hnp-path
└── arm64-v8a/ # device ABI dir
├── python.hnp
└── sub_dir/test.hnp
So shipping Niubash to HarmonyOS PC is not "publish a .hnp and let
people install it". It means publishing a HAP on the app store whose
payload is Niubash's binaries. That is an app-store listing, a signature,
and a review — the same shape of gate as the phone form factor, just with
a looser runtime policy. Anyone who thought "HarmonyOS PC is a tarball"
should recalibrate: the packaging is cheap, the distribution is a
store release.
A public build framework exists —
gitcode.com/OpenHarmonyPCDeveloper/build
— which cross-compiles Linux/Unix CLI tools to aarch64-linux-ohos and
packs them as HNP; it consumes OHOS_SDK, drives everything through
build.sh --sdk <path>, and registers components in a dependency.json.
Precedent components include tree and ninja.
The "same shape as the Linux leg" claim was wrong, twice over: the
artifact is different (niu-<ver>-ohos.hnp, inside a HAP), and the
delivery channel is different (store, not direct download).
Where HNP installs, and whether it lands on PATH
Two install roots, both of which do put their bin on PATH:
| Kind | Install path | Binaries symlinked into | On PATH? |
|---|---|---|---|
| Public HNP | /data/service/hnp/<name>_<version>/ (HNP_PUBLIC_HOME=/data/service/hnp) | /data/service/hnp/bin | ✅ yes |
| Private HNP | /data/app/<name>_<version>/ (HNP_PRIVATE_HOME=/data/app) | /data/app/bin | ✅ yes |
The official guide is explicit: "公有hnp包二进制软链接路径:/data/service/hnp/bin(已加入环境变量)", and the same for
/data/app/bin. Where the two collide, HNP_PRIVATE_HOME wins — a
same-named private binary shadows the public one.
So the open question from the previous revision is closed: a HNP does
put its commands on PATH, and /data/service/hnp/bin — the same
directory the 454-command measurement counted — is that symlink
directory. Tools that install and then say "安装后找不到 ninja 命令?" and
ask you to export PATH=... by hand have gone a manual route; that is not
what the packaging system does by itself.
This is good news for Niubash: no Unix command-directory discovery
mechanism is needed. Once the HAP is installed, the host looks like any
other Unix box and Niubash's existing PATH-based discovery works
unchanged. A real device should still run the smoke gate — but this
particular unknown does not need one.
The build-framework side still uses HNP_PUBLIC_PATH when cross-compiling
on a dev host (e.g. export HNP_PUBLIC_PATH=~/HarmonyOSPC/data/service/hnp)
so hnpcli writes into the right tree; that is a build-host variable, not a
runtime one.
uname on HarmonyOS
uname/arch do not hard-code a platform name. On unix,
crates-side identity.rs reads uname(2) and prints utsname verbatim,
the same way coreutils uname(1) does. So the reported values are whatever
the running kernel says. Measured on real devices:
| Field | Flag | OpenHarmony (Linux kernel) | HarmonyOS (Huawei kernel) |
|---|---|---|---|
| sysname | -s | Linux | HarmonyOS |
| nodename | -n | the host name | the host name |
| release | -r | the kernel release | HongMeng Kernel 1.13.0 |
| version | -v | the build stamp | #1 SMP Sat Aug 15 11:19:26 UTC 2026 |
| machine | -m | aarch64 | aarch64 |
-s being HarmonyOS (not Linux) is the one that bites. config.guess
did not know it, fell through to "unable to guess system type", and
exited non-zero — which breaks every autoconf-based configure, including
LLVM's GetHostTriple.cmake. A patch proposing aarch64-unknown-linux-ohos
for uname -s = HarmonyOS was filed upstream in 2026-08.
-r is a string with spaces. Code that splices uname -r into a
compiler flag without quoting produces -D__HarmonyOS_HongMeng Kernel 1_12_0, which the shell splits into several arguments and Kernel is
mistaken for an input file. That is a real bug in the wild (BitMagic
7.13.4), and the fix was to override the platform label, not to change the
kernel.
There is nothing to special-case in Niubash, and that is deliberate: the
honest source is the kernel, not a compile-time constant. Do not add a
HarmonyOS-specific sysname() branch unless a concrete consumer needs
it — and if something does need to branch, it should match HarmonyOS
exactly, not Linux.
$BASH_VERSION reports Niubash's Bash-compatibility level (e.g.
5.3.0(1)-release) on every platform, HarmonyOS included.
Sandbox quirks worth knowing on HarmonyOS
Beyond the kernel name, HarmonyOS restricts the filesystem in ways a
portable shell has to survive. Measured on HarmonyOS 7 (aarch64):
/tmp is read-only and $HOME is not writable, so a probe that opens a
temp file gets Permission denied. That is why a naive config.guess
fails twice on the same box — once on the temp file, and again on the
unrecognised OS name. Applications also face binary code-signing
verification, which is why third-party executables on HarmonyOS PC
frequently hit Permission denied on launch.
A shell that assumes a writable $TMPDIR or a writable $HOME will
misbehave here. Niubash should be checked against both on the first real
device run; nothing in the build changes.
None of this changes the build. It is listed here so nobody spends a day re-deriving it.
HarmonyOS PC vs HarmonyOS phone
These are two different products, not one port.
| HarmonyOS PC | HarmonyOS phone | |
|---|---|---|
| Userland | full (bash + 454 commands) | full |
fork/exec of new processes | unrestricted | restricted — ordinary apps cannot create processes directly; the app must declare each one in its config and pass per-process review at listing time |
| Packaging | HNP (tarball + PATH is not supported) | HNP, same format |
| Distribution | HAP on AppGallery — an HNP is embedded in a HAP, not installed standalone | HAP on AppGallery |
| Status | the target this page describes | a new form factor; not in scope yet |
The fork restriction is the load-bearing runtime difference. Huawei's
own FAQ (faqs-ability-112) states that on phones the system restricts and
manages fork for ordinary apps, and community reports add that PC does
not restrict it. Niubash is built on a fork+exec model on unix (see
rubash's fd/unix.rs: Command::new + Stdio::from_raw_fd +
pre_exec), so PC is a plain port while the phone form factor needs the
process-declaration path to be sustainable before it is worth building.
But note what the distribution column now says: both are HAP. The packaging and store-release work is largely the same for the two form factors; what differs is the runtime process policy. That is a different split from the earlier "PC is a tarball / phone is an app" framing, and it is the accurate one.
Do not treat "HarmonyOS support" as one item. The store-release work is shared, but the phone form factor adds a per-process declaration and review gate that PC does not — and that gate is not something Niubash controls.
What niu is not: no MSYS runtime
On Windows niu prints MSYS labels: uname -s reports MSYS_NT-…,
$OSTYPE is msys, $MACHTYPE is x86_64-pc-msys. Those labels are a
compatibility persona (rubash#154),
not a runtime. Ecosystem scripts routinely gate on uname -s matching
MINGW*|MSYS*|CYGWIN* or on OSTYPE = msys before they run; the persona
exists so those gates open. It is a default, not a fact —
RUBASH_IDENTITY=native switches the builtins to the native face
(uname -s → Windows_NT, $MACHTYPE → x86_64-pc-windows).
Under the labels there is no MSYS2 runtime: no msys-2.0.dll, no
cygwin1.dll, and nothing inside a child process that converts POSIX argv
for Windows. niu is a pure native Windows build; every process it starts
is an ordinary Win32 process, and the POSIX-form argv an MSYS runtime
would rewrite in-child is instead rewritten by niu's own executor before
that process starts — the distinction the behavioral boundary below
states. So the uname story on this page splits by platform: on unix
the builtin reports the kernel verbatim (see
uname on HarmonyOS); on Windows it reports
persona labels that name a runtime which is not there.
The behavioral boundary (measured)
| A script written for real MSYS assumes | niu does |
|---|---|
| The runtime rewrites POSIX-form argv before a native child sees it. | It does — /c/Users/cmx reaches a native child as C:\Users\cmx (python -c 'import sys; print(sys.argv[1])' /c/Users/cmx). The exception is a native command processor: cmd /c echo /d/repo prints /d/repo, because cmd owns /C-style switches. |
/d/... reaches the child as D:\.... | For a native child, yes — git -C /d/repo/niubash rev-parse --show-toplevel prints D:/repo/niubash, and python -c 'import sys; print(sys.argv[1])' /d/repo prints D:\repo. A native command processor is again the exception (see above). |
An MSYS2 root: /etc/fstab, /etc/profile, MSYS2's /usr. | / is winuxcmd's installation root (ls / lists its usr, bin, etc, opt, …), not the current drive's root; the Unix-shaped /etc resolves inside that tree to a real Windows directory holding only winuxcmd's mtab shim. |
| MSYS2's bundled perl/awk runtimes. | No perl at all; awk is winuxcmd's own native build, not MSYS2's. |
The root row is the one that still bites: niu's / is the WinuxCmd
installation root, so a script that walks out from / sees the winuxcmd
tree (usr, bin, etc, opt, …) rather than a drive root, and there
is no MSYS2 /etc/profile or /etc/fstab. On the argv rows niu now
matches what an MSYS2-targeted script assumes: POSIX-form argv is
rewritten for native children, and shell-namespace roots such as
/usr/bin, /etc, and /tmp are mapped for every child class. The one
carve-out is the native command processor, which keeps its slash-prefixed
drive-form operands verbatim. Unlike an MSYS runtime, though, this is not
a layer inside the child: it is niu's own path handling, applied by the
Rubash executor at the child boundary and documented in the
Windows Path Contract (the internal
__RUBASH_ARGV_DIALECT=legacy switch restores the earlier
existence-gated behaviour).
One line of orientation: Git Bash = MSYS2 runtime + GNU bash; niu = native engine + compatibility persona. The labels match so ecosystem scripts run; the runtime they name is not present.
Calling PowerShell from niu
niu is a full bash, so every command string it runs (through
niu -c '...', a script file, or piped stdin) goes through expansion
first, and bash expands $ in double-quoted and unquoted text.
PowerShell uses $ for the same slot: $var, $args, $env:TEMP. Call
PowerShell without quoting discipline and niu expands PowerShell's
variables before PowerShell starts. An undefined niu variable expands to
the empty string, so this:
niu -c 'powershell -NoProfile -Command "$x=1; Write-Output x=$x"'
hands PowerShell =1; Write-Output x=, which fails with
=1 : The term '=1' is not recognized. $x was never a niu variable, so
bash replaced both occurrences with nothing.
This is standard bash semantics, not a quoting bug; the ruling and the reproduction cases are in niubash#203. The fix belongs to the calling side:
1. Escape the $. Inside double quotes, \$ survives expansion as a
literal dollar sign:
niu -c 'powershell -NoProfile -Command "\$x=1; Write-Output x=\$x"'
# prints x=1
It works, but every PowerShell $ now owes a backslash, and the one you
miss turns into an empty string; the error you get names the mangled
command, not the missing backslash.
2. Keep the $ inside bash single quotes. Bash never expands inside
single quotes. From an interactive niu prompt, one pair does the whole
job:
powershell -NoProfile -Command '$x=1; Write-Output x=$x'
Through niu -c the quoting doubles, because the -c payload is itself
a shell string to whatever shell launches niu. Once you are counting
backslashes at two levels, use the file route instead.
3. Write a .ps1 file and run it with -File (recommended). The
script's text never passes through bash, so nothing needs escaping. The
$t, $o, $env:TEMP case from #203's real-world report runs as
written:
niu -c 'powershell -NoProfile -ExecutionPolicy Bypass -File C:/scripts/scan.ps1'
-ExecutionPolicy Bypass is not optional polish. Windows clients ship
with the script execution policy set to Restricted, which refuses
.ps1 scripts with "running scripts is disabled on this system"; the
flag lifts that check for the one process and changes nothing outside
it.
The rule itself is bash's, not PowerShell's: any target that puts $ in
double-quoted or unquoted command text is subject to it.
What is Windows-only
By compile-time cfg, the following never appear on non-Windows paths and
must not be weakened (no wpm strings off Windows):
- WinuxCmd command links and the
wpmpackage-manager surface. - The ConPTY interactive journey harness.
- Windows path translation (
/mnt/Xdrive argv rewriting) and the Windows signal bridge.
Unix tarballs carry only niu + README.md + LICENSE; native system
tools are used as-is rather than bundled.
tags: [niubash, roadmap] created: 2026-07-13 status: active
Niubash Roadmap
Niubash is a Windows-native, non-isolated Bash-compatible shell for humans and agents.
niubash = rubash + winuxcmd + reedline + oh-my-niu
Done
rubashis embedded as the shell language engine.- WinuxCmd command links provide bundled Unix-style commands through PATH.
- The REPL uses reedline for editing, history, menus, autosuggestions, syntax highlighting, vi/emacs modes, and prompt rendering.
~/.niubashrcis the primary interactive startup file.~/.winshrcis an old fallback only when~/.niubashrcis absent.- Plugin CLI decisions, permissions, bundle versions, tests, and advanced overrides are maintained as internal managed state, not user configuration.
- Plugin inventory, review, doctor, enable/disable, update, and rollback surfaces exist.
oh-my-niuis the official bundled plugin distribution.- Git completions, Git prompt status, prompt templates, and p10-style segment presets are available.
- Host contract tests cover cwd, env, stdin, stdout/stderr, script args, command-mode parsing, and exit-code propagation.
Current Direction
- Keep shell semantics in
rubash; fix parser, executor, builtins, redirects, pipelines, functions, jobs, and Bash language behavior upstream. - Keep Windows host integration in Niubash: cwd/env synchronization, PATH injection, installer behavior, command links, prompt rendering, and REPL UX.
- Model the plugin system after mature shell frameworks: named packs in a bundle, pack manifests, sourceable helper files, exported aliases, completions, functions, hooks, prompt segments, keybindings, and themes.
- Keep prompt layout user/theme owned. Providers such as native Git status or Starship-backed Git supply segment data; they do not replace the whole prompt unless the selected theme chooses that layout.
- Keep third-party integration on reviewed source packs and explicit process adapters until a real host API justifies another runtime.
Near-Term Work
- Normalize first-party pack manifests around exports and permissions.
- Move more alias/completion/theme assets from compiled fallback into the
bundled
oh-my-niudistribution. - Split prompt responsibilities clearly:
- theme owns layout and connective text;
- prompt-core renders templates;
- Git/Starship providers supply data for
{git}and related tokens.
- Add regression tests for installed bundle startup, prompt provider selection, and missing external binaries.
- File and fix WinuxCmd command issues separately from rubash language issues.
Verification
- Fast loop:
cargo fmt --check -p niubash; cargo build --locked; cargo test --workspace --locked - Runtime library:
cargo test -p niubash-runtime --lib --locked - Host contract: run the ignored host suites with WinuxCmd command links in PATH when changing PATH, cwd, env, or command-link behavior.
- Local Bash upstream gate remains local-only and should report 86 total, 86 passed, 0 failed for the Niubash binary under test.
Locked Decisions
- License: MIT.
rubashfollows latestunixwin/rubashmaster.- WinuxCmd stays integrated through PATH injection and command links.
~/.niubashrcis the normal user-authored interactive config.- Machine-managed state is not a human-authored configuration surface.
oh-my-niuis the official bundle, not a fork of another shell framework.