Ps-bash has two modes: the PsBash module and the ps-bash shell. They share the same 77 bash commands and typed object pipeline, but serve different use cases.
The PowerShell module (Install-Module PsBash) adds bash commands directly into your PowerShell session. Use this when you want bash commands available alongside Get-ChildItem, ForEach-Object, and everything else PowerShell offers.
Install-Module PsBash
ls -la # LsEntry objects
ps aux | sort -k3 -rn | head 5 # PsEntry objects, sorted by CPU
cat README.md | grep 'bash' # text search, typed objectsEvery command returns a PSCustomObject with typed .NET properties and a .BashText string property. The console renders BashText (looks like real bash). Your code accesses typed properties.
ls -la
→ Invoke-BashLs
→ [LsEntry] objects
.Name = "README.md" [string]
.SizeBytes = 4521 [int]
.Permissions = "-rw-r--r--" [string]
.Owner = "beagle" [string]
.LastModified = 2024-04-02... [DateTime]
.BashText = "-rw-r--r-- 1..." [string]
Pipeline commands (grep, sort, head, tail, tee) match against BashText but pass through the original typed objects. This is the pipeline bridge.
Since PsBash commands are just PowerShell functions, they work seamlessly with native cmdlets:
# Bash to PowerShell
$files = ls -la | grep '.ps1' | sort -k5 -h
$files | Where-Object { $_.SizeBytes -gt 50KB }
$files | ForEach-Object { Write-Host $_.Name }
# Bash command, PowerShell comparison
$top = ps aux | sort -k3 -rn | head 5
$top | Where-Object { $_.CPU -gt 5.0 } | ForEach-Object { Stop-Process $_.PID -WhatIf }
# PowerShell in, bash out
Get-Content errors.log | grep 'FATAL' | sort | uniq -c# Install from PSGallery
Install-Module PsBash
# Add to your PowerShell profile for permanent access
Add-Content $PROFILE "`nImport-Module PsBash -DisableNameChecking"The standalone ps-bash binary is a full interactive shell. Use it as your terminal shell on Windows — it gives you bash syntax, aliases, a colored prompt, and runs external tools like claude, copilot, and git directly.
The shell runs a persistent PowerShell worker process behind the scenes. When you type a command:
Input
│
├─ Is it an alias? ───→ Expand, re-evaluate
│
├─ Is it a bash builtin or mapped command?
│ (cd, ls, grep, echo, sort, find, ps, ...)
│ ───→ Transpile to PowerShell, execute in worker
│
├─ Is it a pipeline or has redirects?
│ ───→ Transpile the whole pipeline, execute in worker
│
└─ Otherwise (external tool)
(claude, git, npm, dotnet, python, code, ...)
───→ Process.Start() directly on the console
Transpiled commands go through BashParser -> PsEmitter -> IpcWorker -> ps-bash-host. They run inside the persistent PowerShell runspace, so variables and working directory persist between commands.
External tools bypass the worker entirely. They get the real console — stdin, stdout, Ctrl+C, ANSI colors, raw mode, alternate screen buffer. This is why claude, copilot, and git work interactively.
Compact output mode is opt-in for agent contexts that need lower token volume.
Enable it per invocation with --compact-output, or ambiently with
PSBASH_COMPACT_OUTPUT=1. The launcher resolves precedence as:
--compact-output/--no-compact-outputPSBASH_COMPACT_OUTPUT- default off
The resolved value is propagated to the host as PSBASH_COMPACT_OUTPUT=1 or
0, so later output-rendering stages only need to read one setting. Defaults
are unchanged when neither flag nor environment variable is present.
With compact mode on, command output is summarized with the command identity, exit code, timeout status, stdout/stderr line counts, stream labels, stderr and file/line diagnostics, and tail context. Repeated lines and low-value progress or listing noise are collapsed to reduce token volume.
Compact mode also shortens routine status surfaces such as
ps-bash host status. The verbose status form normally prints endpoint,
metadata, process, protocol, build, owner, start time, health, and final state
as separate labeled lines; compact mode keeps those identifiers on one summary
line. In the formatter snapshot this reduces the representative status payload
from 222 characters and about 22 whitespace tokens across 10 lines to 201
characters and about 15 whitespace tokens on 1 line while retaining endpoint,
pid, protocol, build, owner, health, and state.
The representative noisy-failure fixture in OutputCompactorTests reduces a
repeated build log to less than half of the raw character count and less than
half of the estimated whitespace-token count while preserving exit code,
stderr, file/line diagnostics, warnings, failure text, and repetition counts.
Use compact mode for agent and CI log capture where token volume matters. Leave it off when downstream tooling needs exact stdout/stderr bytes, when full progress history is the artifact being inspected, or for human-facing interactive sessions.
Ctrl+~ opens command assist in the interactive editor. By default ps-bash uses
claude -p "{{prompt}}" as the provider. The prompt includes the current line,
cursor position, working directory, shell name, and OS. Provider output is
shown for review and is never executed directly from the provider process.
Provider configuration is loaded from PSBASH_AI_CONFIG when set, otherwise
from $PSBASH_HOME/.psbash/ai-providers.json or ~/.psbash/ai-providers.json.
Multiple providers can be declared and selected with defaultProvider:
{
"defaultProvider": "claude",
"providers": [
{
"name": "claude",
"executable": "claude",
"args": ["-p", "{{prompt}}"],
"timeoutMs": 30000,
"outputLimit": 8192,
"promptTemplate": "Context:\n- input: {{buffer}}\n- cursor: {{cursor}}\n- cwd: {{cwd}}\n- shell: {{shell}}\n- OS: {{os}}\n\nReturn compact JSON only: {\"command\":\"single bash command or empty\",\"explanation\":\"why\",\"refusal\":\"optional\",\"clarification\":\"optional\",\"plan\":[\"optional steps\"]}"
}
]
}Provider processes inherit the shell environment by default. Entries in
environment override variables; a null value removes a variable. Startup
failures, missing executables, nonzero exits, and timeouts are reported at the
prompt and the original line is restored.
The default contract prefers compact JSON with command, explanation,
refusal, clarification, and plan fields. A structured response with a
non-empty command is treated as an executable suggestion. Refusals,
clarifications, plans, malformed JSON, and multi-line explanatory plain text are
shown as review-only text: they can be inserted for editing but cannot be run
with the execute action. Single-line plain text is accepted for compatibility
with simple providers.
ps-bash shows the provider name, current directory, generated text, and any
explanation before asking for an action: execute, insert/edit, retry, switch
provider, or cancel. Commands that match destructive patterns such as recursive
delete, forced git operations, forced overwrite redirects, privilege
escalation, package installation, or network-to-shell pipelines require an
additional EXECUTE confirmation.
To disable command assist, set PSBASH_AI_DISABLE=1; the hotkey will report
that the feature is disabled and restore the original line. Ctrl+~ and Ctrl+^
are both accepted because terminals commonly encode Ctrl+~ as the same control
character. To change providers, set defaultProvider in the config or choose
switch provider from the review prompt.
Troubleshooting:
- Missing provider executable: install the CLI or set
executableto an absolute path. - Slow provider: reduce
timeoutMsor use a faster local provider. - Truncated answer: increase
outputLimit. - Unexpected non-executable review text: return structured JSON with a non-empty
commandfield.
ps-bash --compact-output -c "dotnet test"
$env:PSBASH_COMPACT_OUTPUT = '1'
ps-bash -c "git status"
ps-bash --no-compact-output -c "git status"less is handled as shell-adjacent pager functionality. In a real ps-bash interactive session, less file and producer | less delegate to native less when it is available on PATH; pipeline input is spooled to a temporary file and removed after the pager exits. Native less provides the MVP pager behavior: arrow keys, PgUp/PgDn, / search, resize handling, and q to quit.
In non-interactive contexts such as ps-bash -c "printf 'x\n' | less" or IPC-captured execution, less does not wait for terminal input. Piped input is passed through, and file operands are printed to stdout. If an interactive terminal is attached but native less is missing, ps-bash reports a clear error instead of attempting partial emulation.
- Install the binary:
iwr https://raw.githubusercontent.com/standardbeagle/ps-bash/main/install.ps1 | iex- Add to Windows Terminal — Settings → Add new profile:
| Setting | Value |
|---|---|
| Name | ps-bash |
| Command line | C:\Users\you\.local\bin\ps-bash.exe |
| Starting directory | C:\Users\you |
- Create
~/.psbashrc:
# ~/.psbashrc — sourced on interactive shell launch
alias ll='ls -al'
alias la='ls -a'
alias grep='grep --color=auto'
alias cdsp='claude --dangerously-skip-permissions'
export EDITOR="code"
if [ -f ~/.env ]; then set -a; source ~/.env; set +a; fi- Launch — Open the ps-bash Windows Terminal tab.
andyb@my-pc:~/projects/my-app (main) $
andyb@my-pc— user@host (green, bold)~/projects/my-app— working directory with~substitution (cyan, bold)(main)— git branch (green = clean, red = dirty)$—$for users,#for admin (magenta, bold)
The shell detects incomplete constructs and shows a continuation prompt:
andyb@pc:~/app $ if [ -f package.json ]; then
> echo "found it"
> fi
found it
andyb@pc:~ $ greet() {
> echo "Hello, $1"
> }
andyb@pc:~ $ greet World
Hello, WorldWorks for if/fi, for/do/done, while/do/done, case/esac, functions, braces, parens, and unclosed quotes.
alias ll='ls -al' # define
alias # list all
alias ll # show one
unalias ll # remove
unalias -a # remove allAliases expand the first word of input before the transpiler sees it. So ll becomes ls -al, then ls is mapped to Invoke-BashLs.
As you type, the shell suggests commands from your history in gray (dimmed) text after your cursor:
andyb@pc:~/app $ git comm
it -m "fix parser bug"
Press Right or End to accept the full suggestion, or keep typing to ignore it.
Key behaviors:
- Suggestions match the beginning of history entries (prefix match)
- Commands from your current directory are suggested first
- Most recent commands are preferred when multiple matches exist
- Suggestions are disabled when the tab completion menu is active
- Configure via
~/.psbash/config.toml:
[completion]
autosuggestions = true # Enable (default) or disableThis is fish shell's most praised feature — it dramatically reduces typing for repeated commands. See docs/specs/autosuggestions.md for full details.
Tab completion can show a compact flag/parameter detail panel under the prompt. Use these controls:
F1: open the detail browser for the current command or selected flag.Down: focus the visible detail panel.Up/Down: move within a focused panel.Right: drill into the selected flag or parameter details.Enter: insert the selected flag or parameter.Esc: leave the focused panel and return to editing.
The prompt legend stays compact: unfocused panels show F1 details · ↓ focus;
focused panels show → details next to the scroll/insert controls.
export EDITOR="code" # set in the worker
export PATH="$HOME/.local/bin:$PATH"- Transpiled commands: Cancels execution, prints
^C, restarts the worker - External tools: Passes through to the child process —
claudeandgithandle it themselves - Shell mode: Pressing
Ctrl+Cnever exits the shell; useexitorCtrl+Dto quit
The shell resolves external commands using the merged PATH from your PowerShell profile, so tools installed via package managers (like fnm, nvm-windows, or scoop) are available even though the worker starts with -NoProfile.
opencode # resolves opencode.ps1 from fnm PATH
copilot # resolves copilot.exe from WinGet
claude # resolves claude.exe from ~/.local/binOn Windows, .cmd, .bat, .ps1, and .exe extensions are all resolved automatically.
Start the shell without loading your PowerShell profile PATH:
ps-bash --noprofile
ps-bash --norcThis is useful for CI/CD or when you want a clean environment, matching the behavior of bash --noprofile.
Copy files with automatic Windows binary swap support:
install -m 755 ./build/myapp /usr/local/bin/myapp
install -t ~/.local/bin ./build/myappOn Windows, if the destination file is in use, install renames the old file (.old suffix), copies the new one, and schedules deferred deletion of the old file.
exit, exit N, logout, or Ctrl+D.
Use this guide to decide which mode fits your workflow.
- You already live in PowerShell and want bash commands added to it
- You're writing scripts that mix bash and PowerShell idioms
- You want typed .NET objects from bash commands to pipe into cmdlets
- You're in VS Code's integrated PowerShell terminal
- You're in a CI/CD pipeline running PowerShell
# Your PowerShell profile
Import-Module PsBash -DisableNameChecking
# Now bash commands are just PowerShell functions
ls -la | Where-Object { $_.SizeBytes -gt 1MB }- You want a dedicated terminal that speaks bash natively
- You're running AI coding agents (Claude Code, OpenCode, Copilot)
- You want aliases,
.psbashrc, and a bash-like prompt - You're switching from WSL/Git Bash and want a Windows-native equivalent
- You want to run
claude,copilot, orgeminiinteractively
# Launch ps-bash as your shell
ps-bash
# Inside: bash commands work
ll # alias: ls -al
cd ~/projects && git status # cd + git
# External tools work too
claude # full interactive session- Module for scripts and VS Code, shell for terminal
- They share the same command implementations — what you learn in one transfers to the other
| Module | Shell | |
|---|---|---|
| Install | Install-Module PsBash |
Download binary |
| Access | Inside PowerShell sessions | Standalone terminal |
| Prompts | Your PowerShell prompt | user@host:cwd (branch) $ |
| Aliases | PowerShell aliases | alias builtin + .psbashrc |
| Startup file | $PROFILE |
~/.psbashrc |
| External tools | Via PowerShell (& claude) |
Direct console execution (PATH + PATHEXT) |
| History | PSReadLine (rich) | SQLite with Ctrl+R fuzzy search |
| Tab completion | Full (flags, files) | Commands, flags, files, sequences |
| AI agent use | No | Yes (SHELL=ps-bash) |
| Multi-line | PSReadLine handles it | Built-in > continuation |
| Variable sharing | Native PS variables | Variables in the worker session |
.psbashrc |
Not sourced | Sourced on startup |
Ctrl+C |
PS handles it | Cancels command; never exits shell |
--noprofile |
N/A | ps-bash --noprofile |
Since the shell's worker is a persistent PowerShell session, you can freely mix syntaxes:
ls -la | grep '.cs' | sort -k5 -h
cat README.md | wc -l
find . -name '*.ps1' -type fAnything the bash parser doesn't recognize passes through to PowerShell:
Get-Process | Where-Object { $_.CPU -gt 5 }
$PSVersionTable
Write-Host "hello"
Get-Date -Format 'yyyy-MM-dd'
# bash produces typed objects, PowerShell filters them
ls -la | Where-Object { $_.SizeBytes -gt 100KB }
# bash processes, PowerShell stops them
$top = ps aux | sort -k3 -rn | head 5
$top | ForEach-Object { Stop-Process $_.PID -WhatIf }| From | To | Example |
|---|---|---|
| Bash → Bash | ls | grep foo |
Fully transpiled, typed objects flow |
| PS → PS | Get-Process | Sort-Object CPU |
Passed through verbatim |
| Bash → PS | ls -la | Where-Object { $_.SizeBytes -gt 1MB } |
Bash objects, PS filter |
| PS → Bash | Get-Content log | grep ERROR |
PS output, bash filter |
# bash variable assignment
files=$(find . -name '*.cs' -type f)
# access in PowerShell context
$files | Measure-Object -Line
# PowerShell variable (set in the worker)
$procs = Get-Process
# use in next bash command's $() substitutionVariables persist across commands because the worker session is long-lived.
These are the known gaps in priority order. Each includes an estimated effort and what would be involved in fixing it.
The shell uses .NET's Console.ReadLine() which has no history buffer. Pressing Up does nothing.
Fix: Replace Console.ReadLine() with a readline library that supports history, reverse-search (Ctrl+R), and line editing. Options:
- Use
System.ReadLineNuGet package - Port a minimal GNU readline wrapper
- Use PowerShell's PSReadLine via the worker
Impact: This is the single biggest UX gap. Every shell needs Up-arrow history.
The module has full tab completion for flags (ls -[Tab]), but the shell has none. No command completion, no file completion, no flag completion.
Fix: The worker already has the PsBash module loaded with TabExpansion definitions. Wire Console.TreatControlCAsInput + Tab key detection to request completions from the worker via a protocol message, then render inline.
ls *.txt works because Invoke-BashLs uses -Filter. cat *.txt works because PowerShell resolves it. But rm *.bak or mv *.tmp /tmp/ may not expand as expected because glob resolution depends on the individual command implementation.
Fix: Add a shell-level glob expansion step before sending to the worker — expand *.txt to the matching files using Directory.GetFiles(), then pass the expanded list to the command.
Only the first word of input is checked for alias expansion. sudo ll won't expand ll. git commit -m "fix" && ll won't expand ll after &&.
Fix: After splitting on &&, ||, ;, and |, expand aliases on the first word of each segment.
EDITOR=nano crontab -e has env pairs, so TryRunDirect rejects it and sends it through the worker. The worker handles env vars then runs the command — but it runs inside the pipe protocol, not on the console. Interactive tools launched this way won't get a terminal.
Fix: In TryRunDirect, extract env pairs, set them as environment variables on the ProcessStartInfo, and still run directly.
git log $BRANCH or rm *.bak contain non-literal word parts, so TryRunDirect rejects them. The worker handles them fine, but the command loses direct console access.
Fix: For globs, expand before checking. For variables, resolve them by sending a quick $var query to the worker first. Or accept the fallback for variable cases (the worker handles them correctly).
The prompt format is hardcoded in C#. Users can't configure colors, layout, or add custom segments.
Fix: Support PROMPT_COMMAND or a .psbashrc prompt function. The shell evaluates the function each cycle and uses the result instead of the built-in prompt.
No mechanism to run code before each prompt display (for custom window titles, tmux integration, etc.).
Fix: After each command completes and before displaying the prompt, evaluate a PROMPT_COMMAND variable from the worker.
The PowerShell worker has its own $PWD, $LASTEXITCODE, variables. The shell tracks _lastDir separately by parsing cd commands. If the worker's CWD drifts (e.g., a PowerShell function changes it), the prompt shows the wrong directory.
Fix: After each command, query the worker's $PWD and sync _lastDir. This also fixes pushd/popd prompt tracking.
| # | Limitation | Effort | Impact |
|---|---|---|---|
| 1 | No command history | Small | Critical UX — every shell needs this |
| 2 | No tab completion | Medium | Major UX — expected in any modern shell |
| 3 | Glob expansion inconsistent | Small | High — rm *.bak should just work |
| 4 | First-word-only alias expansion | Small | Medium — sudo ll is common |
| 5 | FOO=bar cmd falls back to pipe |
Small | Medium — needed for env-prefixed tools |
| 6 | Variable/glob args fall back | Small | Low — worker handles it correctly |
| 7 | No customizable prompt | Small | Medium — personalization matters |
| 8 | No preexec hook | Small | Low — niche feature |
| 9 | Worker state not synced | Medium | Medium — causes subtle prompt bugs |
Items 1, 3, 4 are the highest-impact, smallest-effort wins. They would make the shell feel substantially more complete.