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:\... and C:/...), direct execution of git.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/repo arrives at python.exe as D:\repo, while shapes the executor does not classify as paths (/nonexistent and /fn main) arrive byte for byte. The native command processor keeps its /C-style switch-position operands verbatim. The full contract — the exact rules, the cmd.exe carve-out, and the __RUBASH_ARGV_DIALECT=legacy pass-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 -c is 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

GuideWhat it covers
Getting StartedZero to a git prompt in ten minutes
Advanced UsageExecution modes, startup files, themes, plugins, completion, debugging
Install & Self-UpdateInstaller, portable zip, Windows Terminal profile, updates
Bash Compatibility MatrixWhat Bash surface is verified, layer by layer
Built-ins & Fast PathsAll 64 built-ins, fast-path fallbacks, WinuxCmd overlap
Architecturerubash + WinuxCmd + reedline, path model, host contract
Windows Path Contractlogical root, dispatcher selection, and layer ownership
RoadmapWhat 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:

OptionThe fine print
CMDFrozen in 1987. Your company's docs still paste it.
PowerShellA genuinely powerful automation language. It is not Bash — your for loops, grep, and quoting instincts die on arrival.
WSLA 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 / MSYS2Brilliant 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, ...).

NiubashWSLGit BashPowerShell
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 msseconds~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.

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:

Git Bash failing: backslash paths eaten, /c/ paths rejected by cmd, MSYS path conversion breaking git grep

What just happened, command by command:

CommandIn Git Bash (MSYS)In Niubash
node -e "..." "C:\Users\you\repo"the backslashes are eaten — the argument vanishesargument 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

#MechanismReadsCovered by launching from niu
1$SHELL (POSIX convention)the environmentyes
2Explicit per-agent settingconfig file / CLI flagn/a, set it once
3Git for Windows probehardcoded paths, or derived from gitno
4Bare name, a table, or PowerShell by defaultPATH, ComSpec, PATHEXTpartly

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 -c invocation (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 setting CREATE_NO_WINDOW when 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 seeShellNotes
uname -s = MSYS_NT-..., uname -a ends x86_64 Msysniu (winuxcmd shim)what you want; $SHELL points at niu.exe
uname -s = MINGW64_NT-..., MSYSTEM=MINGW64Git BashGit for Windows' own runtime
uname -s = LinuxWSLthe surprising one; the agent reached System32\bash.exe
uname not found, $SHELL emptyPowerShell or cmdcheck $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:

  • Linux means the agent fell through to which("bash"). Fix the mechanism that agent uses, below.
  • MINGW64_NT means it found Git Bash. If you want niu instead, set the agent's explicit shell (mechanism 2) or the $SHELL variable.
  • Empty $SHELL with PowerShell means the agent defaults to PowerShell, which is safe but not bash-compatible.

Agent reference

opencode

  • Mechanism: $SHELL first, then which("bash"), then /bin/sh. Its gitbash mode 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-path up three levels plus bin\bash.exe and 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_PATH it looks for bash.exe in (1) C:\Program Files\Git and C:\Program Files (x86)\Git, then (2) the git on PATH, using bin\bash.exe from that installation. It skips a git sitting in the launch directory or below it, or under node_modules/.venv/env.
  • Set it to niu: either works today. Probe (1) hits the bash.exe the installer places in C:\Program Files\Git\bin, or set CLAUDE_CODE_GIT_BASH_PATH in settings.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, bash or sh. Point it at git-bash.exe and it is ignored with a warning. Pointing it at niu.exe is likewise ignored, so the launcher hop cannot be skipped by naming niu directly; that is another reason the argv[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::Niu with C:\Program Files\Niubash\niu.exe, C:\Program Files (x86)\Niubash\niu.exe, %LOCALAPPDATA%\Programs\Niubash\niu.exe and a niu lookup on PATH; the exec policy also whitelists niu.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.windows and terminal.integrated.automationProfile.windows and 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 with terminal.integrated.useWslProfiles: false.
  • Agent CLI: hardcoded to PowerShell. No --shell flag, no shell field in ~/.cursor/cli-config.json, and $SHELL/$COMSPEC ignored. The shipped detectShellType() puts isWindows && (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 working NaiveTerminalExecutor exists 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.asar read locally; the rest is the same lineage.

Gemini CLI

  • Mechanism: getShellConfiguration() returns PowerShell unconditionally on Windows. ComSpec is only consulted to decide between pwsh and powershell.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 to bash -c (issue #26567), and prefer Git Bash by searching PATH for a bash.exe whose path contains git (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-niu ships a profile bundle that enables niu-sandbox and tool-bash, disables the PowerShell shell/tool rows, and adds a settings card. A third party ships dsh-niubash-only in 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.program names the shell, and the agent uses that terminal.
  • Set it to niu: point terminal.shell.program at niu.exe.
  • Source: settings in use on this machine.

Aider

  • Mechanism: there is no shell selection option. /run uses PowerShell or cmd.exe on Windows, and the model-visible environment reports Shell: COMSPEC=.... --suggest-shell-commands and --shell-completions are 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

PathServesOwnerNeeds admin
SHELL=%LOCALAPPDATA%\Programs\Niubash\niu.exe (user env)mechanism 1installerno
%LOCALAPPDATA%\Programs\Niubash\bin\{bash,sh}.exeopencode-shaped derivation when git resolves from winuxcmd\bininstaller / niu setupno
C:\Program Files\Git\bin\bash.exemechanism 3 probe 1 (Claude Code, VS Code family)installeryes
C:\Program Files (x86)\Git\bin\bash.exethe second hardcoded candidateinstalleryes
C:\Program Files\Git\bin\sh.exetools that want sh beside the bash they foundinstalleryes
...\winuxcmd\opt\niugit\bin\{bash,sh}.exeopencode's updater probe (git --exec-path up three)niugit package manifest, so wpm refresh keeps itno

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:

Callerbash becomes
cmd /c bash -c 'uname -s'winuxcmd's bash (MSYS_NT-...)
PowerShell & bashwinuxcmd'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.

  1. Installer writes SHELL to 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.
  2. niu setup agents (idempotent) creates the non-elevated paths, prints what each probe resolves to, and says which entries need elevation.
  3. Installer's optional elevated step creates the Program Files\Git entries, with the second candidate and sh.exe.
  4. Per-agent settings for the agents that read neither $SHELL nor 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

  • MSYSTEM is 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.
  • uname in the winuxcmd shim reports MSYS_NT-.... Anything that branches on uname will 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/sh in CLAUDE_CODE_GIT_BASH_PATH.
  • A bash that 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:

SymbolMeaning
●NN files staged for commit
✚NN files modified but unstaged
?NN untracked files
↑NN commits ahead of upstream
↓NN commits behind upstream
⚑NN stashes saved
✖NN 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

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 -c for scripts, tests, CI, and coding agents. It does not load ~/.niubashrc, ~/.winshrc, prompt plugins, or interactive lifecycle hooks.
  • Use -C only when a one-shot command needs the same startup state as the interactive REPL. It loads ~/.niubashrc and 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:

  • ~/.winshrc is a fallback only when ~/.niubashrc is 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 zsh compinit).
  • list — vertical list with descriptions (like fish).
  • inline — first match inserted directly, Tab cycles (like bash menu-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.
  • session keeps the startup snapshot stable for navigation while builtins can observe later file updates.
  • private loads 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) and NIU_WIDGET_CURSOR (byte offset). Both are temporary and restored afterwards.
  • Out: NIU_WIDGET_RESULT replaces the buffer (unset or absent keeps it; an empty string clears the line), NIU_WIDGET_CURSOR_RESULT moves the cursor to a byte offset, and NIU_WIDGET_ACCEPT=1 submits 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 bash COMP_WORDS) and NIU_COMP_CWORD (bash COMP_CWORD semantics). Both are temporary.
  • Out: NIU_COMP_RESULT, one candidate per line, shaped value or value<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_ROOT overrides 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-update updates the shell.
  • wpm update winuxcmd updates command packages and command links.
  • niu plugin update <source-id> updates an installed plugin source; rollback returns 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

NounWhat it isWhere it lives
SourceA git tree installed by niu (a manager like oh-my-bash, or any single-purpose repo)~/.niubash/sources/<id>/
AssetA theme/plugin/alias/completion inside a manager sourceenabled via the manager's own mechanism
RecipeA data row describing one ecosystem asset and how to install it (~500-row compiled-in index)niu plugin recipe list
CollectionA 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 --json and niu 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 sync reconciles the spec with reality; niu plugin restore rebuilds from the lockfile pin in ~/.niubash/sources/registry.toml.
  • Exit codes: success 0; usage errors and refused operations (unknown id, builtin-name collision, non-tty ui) 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 --help transcripts;
  • common external commands — git, tar, curl, ssh, docker, kubectl, and npm — as static subcommand/flag tables (issue #305): git co<Tab> offers checkout/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 pointBash-native mechanismNotes
before every promptPROMPT_COMMAND (function or string)runs each prompt cycle, before the prompt is rendered
before a command executesPS0 (expanded, printed)after you press Enter, before execution
prompt identityPS1 assignmentclaims the prompt slot (defaults-as-floor: the product floor never fights an active claim; unset PS1 restores it)
directory changechpwd-style: cd hooks via PROMPT_COMMAND + PWD diffing in your own functionno dedicated chpwd hook yet
signals / exittrap 'handler' INT TERM EXIT DEBUG ERRfull bash trap semantics, via the engine
terminal titlewrite 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.exe for normal users.
  • niubash-v<version>-win-<arch>.zip for 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.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.exe there 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\bin never goes on PATH. It holds the whole Unix command set; putting it there shadows git, cygpath, find, sort, sed and 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, and niu --relink-shells keeps the shell faces on the current niu.exe inode. 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 on PATH must 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

LayerScopeCurrent evidence
Local compat fixturesFocused 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 testsWindows 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 gateBroader 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

CapabilityEvidence fixture(s)StatusBoundary
Variables and simple parameter expansionvar_expansion, string_paramPassingrubash parser/executor.
Command substitutioncommand_substitution, command_substitution_quoted_newline, command_substitution_function_pipelinePassingrubash command substitution; host still owns -c quoting and process invocation.
Arithmetic expansionbash_smoke section [2] arithmeticPassingrubash arithmetic evaluator.
Indexed arraysbash_smoke sections [3] arrays, [16] array slicePassingrubash arrays and parameter expansion.
Associative arraysbash_smoke section [4] assoc arraysPassingrubash declare -A and associative lookup.
Boolean list statusand_or_status, bash_smoke section [20] exit statusPassingrubash &&, `
If / elif / elseif_else, multiline_if, bash_smoke section [11] ifPassingrubash compound commands; Niubash must feed full scripts to rubash.
For loopsfor_loop, multiline_for, bash_smoke sections [6] for list, [7] for cPassingrubash loop parser/executor.
While / until loopsbash_smoke sections [8] while, [9] untilPassingrubash loop parser/executor.
Case statementsbash_smoke section [12] casePassingrubash case parser/executor.
Functionsfunction, command_substitution_function_pipeline, bash_smoke section [10] functionPassingrubash function definition and invocation.
AliasesaliasPassingNiubash installs aliases into rubash; expansion is rubash-owned.
Pipelinespipeline, command_substitution_function_pipeline, bash_smoke section [13] pipelinePassingrubash pipeline execution plus winuxcmd command links.
Redirectionbash_smoke section [14] redirectPassingrubash redirection with Niubash/winuxcmd filesystem behavior.
HeredocsheredocPassingrubash whole-script parsing; host stdin/script path must avoid line-by-line splitting.
Backslash continuationscontinuationPassingrubash whole-script parsing.
Echo flagsecho_flagsPassingshell builtin behavior as exposed through rubash/winuxsh.
Export to Windows child processbash_smoke section [19] exportPassingrubash environment plus Niubash process environment synchronization.
File testsbash_smoke section [18] file testsPassingrubash test builtin plus host filesystem paths.

Host Contract Coverage

Host surfaceEvidenceNotes
cwd authoritycwd_cd_pwd_and_windows_child_process_agree, drive_only_cd_and_bare_drive_commands_switch_to_drive_rootNiubash normalizes/synchronizes shell PWD with Windows child process cwd.
startup isolationwinshrc_does_not_run_for_non_interactive_modesNon-interactive -c, script file, and stdin script paths do not source REPL startup rc.
temporary assignmentstemporary_assignment_reaches_nested_winuxsh_childAssignment semantics are observable by nested Niubash child processes.
stdin scriptspiped_stdin_without_args_runs_plain_script_surface, piped_stdin_without_args_runs_multiline_compound_block, piped_stdin_without_args_runs_heredoc_as_one_chunkHost feeds complete stdin scripts to rubash for multiline/heredoc semantics.
script positional parametersscript_file_args_populate_positional_parametersHost script path preserves $0/positional parameter behavior.
Windows child envexported_env_reaches_windows_child_processes, sourced_rc_keeps_winuxcmd_visible_to_windows_childrenNiubash bridges rubash env changes into Windows child process launches.
stdio and exit codestdout_stderr_and_exit_code_are_preserved, closed_stdout_pipe_does_not_print_broken_pipe_errorHost preserves process surfaces expected by agents.
command-mode parsing edge casescommand_mode_accepts_base_prefixed_arithmetic_in_function_body, command_mode_parameter_pattern_removal_handles_escaped_quotes, command_mode_set_positional_splits_custom_ifsFocused regressions for rubash-facing -c script delivery.

Known Gaps and Routing

GapRoute
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.

LayerWhat it isIntrospection (type, command -v, enable)
Real built-inRuns in-process, no child process. Listed in the rubash whitelist (src/executor/builtin_names.rs).Reports as builtin
Fast pathHidden 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 commandResolved 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:

GroupCommands
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 extensionsetopt unsetopt
In-process envenv (coreutils semantics, sorted output, -0)
Windows-onlysudo (not counted in the 64)

Notes:

  • declare/typeset and source/. are alias pairs; [ is implemented by the test builtin and validates the closing ].
  • time, [[, and (( are reserved words / grammar, not built-ins — same as Bash. time honors TIMEFORMAT.
  • The sudo built-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:

CommandIn-process behaviorFallback
sleepFractional seconds supportedexternal sleep
dirnamePlain pathsexternal dirname
basenamePlain pathsexternal 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:

  1. Read the explicit WINUXCMD_PATH, then fall back to Niubash's own install/bundle/PATH discovery rules to locate a winuxcmd.exe.
  2. Prepend the directory of that same exe to the process PATH so that command links such as ls/cat/grep resolve.
  3. 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

  • ~/.niubashrc is the primary interactive entry point. Plain niubash/bash syntax declares plugin lists, theme, prompt template, export, alias, functions, and local startup logic.
  • ~/.winshrc is the compatibility fallback; it is read as the legacy user rc only when ~/.niubashrc does 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 ~/.niubashrc exists, 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.toml is the source of truth for what is declared; the registry pins installed trees (commit + checksum), and niu plugin sync reconciles the two without ever granting trust automatically.
  • Plugins cannot extend the rubash parser/executor, and cannot source arbitrary legacy .winsh files 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

Aspectv1 (old niubash)v2 (current niubash)
Shell enginein-house winsh lexer/parser/astrubash lib
Coreutilswinuxcmd FFI (DLL, disabled)winuxcmd.exe process (PATH injection)
Command routingcommand_router.rs classification tablerubash-internal find_user_command
Built-insself-implemented builtins.rsrubash::builtins
Completionsrc/completion/fully preserved, migrated
Themestheme.rs (8 themes)built-in stack retired (niubash#145); themes come from external sources
PluginsPlugin trait + Oh-My-Niubashexternal bash ecosystem as sources (add/trust/enable; no bundles)
LicenseMITGPL-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-niu bundle 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

LayerResponsibility
NiubashSelect one winuxcmd.exe, derive its installation root, create the real tree, and configure the shell session.
RubashInterpret /, /bin, /usr/bin, /etc, /tmp, cd, source, glob, redirects, tests, and command lookup.
WinuxCmdImplement external commands and native Windows filesystem, process, handle, and device operations.
WPMInstall 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.exe tree 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-preserving pwd. The executable-leaf convention is theirs too: /usr/bin/seq resolves to the real seq.exe while ls -d keeps the operand spelling, so head -1 /usr/bin/seq and test -f /usr/bin/seq agree (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/repo or /c/Users reaches cmd.exe verbatim, because /d and /c are indistinguishable from cmd's own switches. Root forms and /mnt/X spellings are still translated, so cmd /c echo /usr/bin and cmd /c echo /mnt/c/Users arrive native while cmd /c echo /d/repo does not. The exemption is pinned in Rubash (is_windows_command_processor, and the windows_cmd_switches_are_not_mapped_into_shell_root test) and applies to cmd.exe alone: a native program that is not cmd receives /d/repo as D:\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

PlatformBuildSmoke-gatedReleasedNotes
Windows x64✅ windows-2025✅ release gate✅ .zip + .exeprimary target
Windows arm64✅ windows-2025✅ release gate✅
Linux x86_64 (glibc)✅ ubuntu-22.04✅ release gate✅ .tar.gzglibc ≥ 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-ohos reports target_os = "linux" and target_family = "unix". So Niubash's existing unix leg and rubash's existing Linux leg apply nearly unchanged, and fd/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 / restrictedNote
apt / yum / dnfno classic package manager; use hnpcli or preinstalled
systemctlno systemd; HarmonyOS has its own service management
iptables / nftablesrestricted
dockernot preinstalled
snap / flatpakunsupported
ifconfig / netstatpresent 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 --version on a HarmonyOS PC prints GNU 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.gz and configures it with --host aarch64-unknown-linux-musl --without-bash-malloc --disable-nls. --without-bash-malloc matters: it drops bash's bundled allocator so the musl allocator is used instead.
  • The build emits libsh.a, libhistory.a and the loadables (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 toybox command 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 PCOpenHarmony dev boards
Shellbashmksh
Command set454 commands at /data/service/hnp/bin (~93%)toybox, per-device trimmed
Text processingfully coveredgaps; awk/grep absent, diff may be "unknown" though the source exists
What Niubash addsa consistent bash + GNU-toolset baseline across OS versions, plus the Bash semanticsthe 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:

KindInstall pathBinaries symlinked intoOn 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:

FieldFlagOpenHarmony (Linux kernel)HarmonyOS (Huawei kernel)
sysname-sLinuxHarmonyOS
nodename-nthe host namethe host name
release-rthe kernel releaseHongMeng Kernel 1.13.0
version-vthe build stamp#1 SMP Sat Aug 15 11:19:26 UTC 2026
machine-maarch64aarch64

-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 PCHarmonyOS phone
Userlandfull (bash + 454 commands)full
fork/exec of new processesunrestrictedrestricted — ordinary apps cannot create processes directly; the app must declare each one in its config and pass per-process review at listing time
PackagingHNP (tarball + PATH is not supported)HNP, same format
DistributionHAP on AppGallery — an HNP is embedded in a HAP, not installed standaloneHAP on AppGallery
Statusthe target this page describesa 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 assumesniu 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 wpm package-manager surface.
  • The ConPTY interactive journey harness.
  • Windows path translation (/mnt/X drive 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

  • rubash is 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.
  • ~/.niubashrc is the primary interactive startup file.
  • ~/.winshrc is an old fallback only when ~/.niubashrc is 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-niu is 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-niu distribution.
  • 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.
  • rubash follows latest unixwin/rubash master.
  • WinuxCmd stays integrated through PATH injection and command links.
  • ~/.niubashrc is the normal user-authored interactive config.
  • Machine-managed state is not a human-authored configuration surface.
  • oh-my-niu is the official bundle, not a fork of another shell framework.