A Python wrapper around the just-bash virtual shell
Project description
just-py-bash
Python bindings for just-bash.
just-py-bash gives Python code the same long-lived virtual shell that upstream just-bash exposes in TypeScript, with two public API styles:
Bashfor synchronous codeAsyncBashfor native-asynciocode
Each session owns a dedicated Node.js worker process and a real upstream just-bash Bash instance. That means upstream semantics are preserved:
- each
exec()call gets its own isolated shell state - the virtual filesystem is shared across
exec()calls - results come back as structured Python objects
Install
uv add just-py-bash
Install name: just-py-bash
Import name: just_bash
By default, just-py-bash uses a system-provided Node.js runtime.
For an explicit bundled-Node install:
uv add 'just-py-bash[node]'
That extra installs the first-party just-bash-bundled-runtime companion package.
js-exec/JavaScriptConfig(...)require Node.js >= 22.6. If the resolved runtime is older,just-py-bashraisesUnsupportedRuntimeConfigurationErrorwith upgrade guidance. Installingjust-py-bash[node]satisfies this automatically.
Quick Start
Synchronous API
from just_bash import Bash
with Bash(cwd="/workspace") as bash:
bash.exec("export NAME=alice; echo 'hello from the shared filesystem' > greeting.txt; cd /tmp")
result = bash.exec(
'printf "name=%s cwd=%s file=%s\n" "${NAME:-missing}" "$PWD" "$(cat greeting.txt)"',
)
print(result.stdout, end="")
bash.fs.write_text("note.txt", "written via bash.fs\n")
print(bash.read_text("note.txt"), end="")
Asynchronous API
AsyncBash is implemented with native asyncio subprocesses, tasks, futures, and locks.
import asyncio
from just_bash import AsyncBash
async def main() -> None:
async with AsyncBash(cwd="/workspace") as bash:
await bash.exec("export NAME=alice; echo 'hello from async shared filesystem' > greeting.txt; cd /tmp")
result = await bash.exec(
'printf "name=%s cwd=%s file=%s\n" "${NAME:-missing}" "$PWD" "$(cat greeting.txt)"',
)
print(result.stdout, end="")
await bash.fs.write_text("note.txt", "written via async bash.fs\n")
print(await bash.read_text("note.txt"), end="")
asyncio.run(main())
Custom Commands
Upstream just-bash uses defineCommand(...). The Python wrapper exposes the same capability with a Python mapping of command names to callables.
Sync custom commands
from just_bash import Bash, CustomCommandContext
def greet(args: list[str], ctx: CustomCommandContext) -> dict[str, str | int]:
del ctx
name = args[0] if args else "world"
return {"stdout": f"hello, {name}!\n", "exit_code": 0}
with Bash(custom_commands={"greet": greet}) as bash:
result = bash.exec("greet mars")
print(result.stdout, end="")
Async custom commands
import asyncio
from just_bash import AsyncBash, AsyncCustomCommandContext
async def annotate(args: list[str], ctx: AsyncCustomCommandContext) -> dict[str, str | int]:
label = args[0] if args else "note"
nested = await ctx.exec("wc -w", stdin=ctx.stdin)
words = nested.stdout.strip().split()[0] if nested.stdout.strip() else "0"
return {"stdout": f"[{label}] words={words}\n", "exit_code": 0}
async def main() -> None:
async with AsyncBash(custom_commands={"annotate": annotate}) as bash:
result = await bash.exec("printf 'one two three' | annotate summary")
print(result.stdout, end="")
asyncio.run(main())
Custom commands can:
- receive shell arguments
- read
ctx.stdin,ctx.cwd, andctx.env - run nested shell commands with
ctx.exec(...) - participate in pipelines and redirections
- override built-in command names if desired
- return non-zero exit codes
- raise exceptions, which become shell failures
Supported Commands
The wrapper delegates command execution to upstream just-bash, so the Python API gets the same command families. For programmatic introspection, use:
from just_bash import (
get_command_names,
get_javascript_command_names,
get_network_command_names,
get_python_command_names,
)
print(len(get_command_names()))
print(sorted(get_network_command_names()))
print(sorted(get_python_command_names()))
print(sorted(get_javascript_command_names()))
Current upstream command categories
File Operations
cat, cp, file, ln, ls, mkdir, mv, readlink, rm, rmdir, split, stat, touch, tree
Text Processing
awk, base64, column, comm, cut, diff, expand, fold, grep (+ egrep, fgrep), head, join, md5sum, nl, od, paste, printf, rev, rg, sed, sha1sum, sha256sum, sort, strings, tac, tail, tr, unexpand, uniq, wc, xargs
Data Processing
jq (JSON), sqlite3 (SQLite), xan (CSV), yq (YAML/XML/TOML/CSV)
Optional Runtimes
js-exec (requires javascript=True / JavaScriptConfig(...)), python3 / python (requires python=True)
Compression & Archives
gzip (+ gunzip, zcat), tar
Navigation & Environment
basename, cd, dirname, du, echo, env, export, find, hostname, printenv, pwd, tee
Shell Utilities
alias, bash, chmod, clear, date, expr, false, help, history, seq, sh, sleep, time, timeout, true, unalias, which, whoami
Network
curl, html-to-markdown (require network=...)
All commands support --help for usage information.
Configuration
The Python API mirrors the upstream configuration model, adapted to Python types and keyword arguments.
from just_bash import Bash, ExecutionLimits, JavaScriptConfig
bash = Bash(
files={"/data/file.txt": "content"},
env={"MY_VAR": "value"},
cwd="/app",
execution_limits=ExecutionLimits(max_call_depth=50),
python=True,
javascript=JavaScriptConfig(bootstrap="globalThis.answer = 42;"),
)
result = bash.exec("echo $TEMP", env={"TEMP": "value"}, cwd="/tmp")
result = bash.exec("cat", stdin="hello from stdin\n")
result = bash.exec("env", replace_env=True, env={"ONLY": "this"})
result = bash.exec("grep", args=["-r", "TODO", "src/"])
result = bash.exec("cat <<EOF\n indented\nEOF", raw_script=True)
result = bash.exec("while true; do sleep 1; done", timeout=5)
bash.close()
Session options
files: initial in-memory files; values can be plain text/bytes,FileInit(...), orLazyFile(...)env: initial environmentcwd: starting directoryfs: upstream-style filesystem config object (InMemoryFs,OverlayFs,ReadWriteFs,MountableFs)execution_limits: validated execution protection settingspython=True: enablepython/python3javascript=TrueorJavaScriptConfig(...): enablejs-execcommands: allowlist commandscustom_commands: register Python-defined commandsnetwork: configure allow-listed network accessprocess_info: process metadata passed to the backend
Filesystem configuration objects
The wrapper now exposes upstream-style init-time filesystem config objects:
InMemoryFs(files=...)OverlayFs(root=..., mount_point=..., read_only=...)ReadWriteFs(root=...)MountableFs(base=..., mounts=[MountConfig(...)])
On Windows, host-backed filesystem configs (
OverlayFs,ReadWriteFs, andMountableFscontaining those) raiseUnsupportedRuntimeConfigurationError. Upstreamjust-bashhost filesystem semantics are currently unstable on Windows, sojust-py-bashfails early with a clear explanation instead of letting file operations silently misbehave. UseInMemoryFs, avoid host-backed mounts, or run under WSL / another POSIX environment.
files= and InMemoryFs(files=...) both support richer initial values too:
- plain text / bytes
FileInit(content=..., mode=..., mtime=...)LazyFile(provider=...)
LazyFile(provider=...) accepts either:
- static deferred content (
str/bytes), or - a Python callable returning
str/bytes(sync or async)
These are serialized when the session starts and decoded into real upstream just-bash filesystem instances inside the Node worker. The filesystem config classes are config objects, not live Python filesystem implementations.
from just_bash import Bash, MountConfig, MountableFs, OverlayFs, ReadWriteFs
with Bash(
fs=MountableFs(
mounts=[
MountConfig(
mount_point="/workspace",
filesystem=ReadWriteFs(root="/tmp/project"),
),
MountConfig(
mount_point="/docs",
filesystem=OverlayFs(
root="/path/to/docs",
mount_point="/",
read_only=True,
),
),
],
),
cwd="/workspace",
) as bash:
result = bash.exec("cp /docs/README.md ./README.copy.md && ls")
print(result.stdout, end="")
If you pass both files= and fs=, upstream just-bash semantics apply and fs= takes precedence.
Session filesystem API
Each session now also exposes a session-bound filesystem proxy at bash.fs / async_bash.fs.
Available methods:
read_text(path)read_bytes(path)write_text(path, content)write_bytes(path, content)append_text(path, content)append_bytes(path, content)exists(path)stat(path)→FsStatlstat(path)→FsStatmkdir(path, recursive=False)readdir(path)readdir_with_file_types(path)→list[DirentEntry]rm(path, recursive=False, force=False)cp(src, dest, recursive=False)mv(src, dest)resolve_path(path, *, base=None)get_all_paths()chmod(path, mode)symlink(target, link_path)link(existing_path, new_path)readlink(path)realpath(path)utimes(path, atime, mtime)
Paths are resolved with upstream just-bash session semantics, so relative paths are interpreted against the session cwd.
from datetime import UTC, datetime
from just_bash import Bash, FileInit, LazyFile
with Bash(
files={
"seed.txt": FileInit(
content="seed\n",
mode=0o640,
mtime=datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC),
),
"lazy.txt": LazyFile(provider=lambda: "lazy content\n"),
},
cwd="/workspace",
) as bash:
bash.fs.mkdir("docs")
bash.fs.write_text("docs/note.txt", "hello\n")
bash.fs.cp("docs/note.txt", "copy.txt")
bash.exec("ln -s copy.txt link.txt")
stat = bash.fs.stat("copy.txt")
print(stat.mode)
print(bash.fs.readlink("link.txt"))
print(bash.fs.realpath("link.txt"))
Per-exec options
env: environment variables for this execution onlycwd: working directory for this execution onlystdin: standard input passed to the scriptargs: argv passed directly to the first commandreplace_env: start with an empty environment instead of mergingraw_script: skip leading-whitespace normalizationtimeout: cooperative timeout in seconds
AsyncBash.exec(...) accepts the same options; you just await the call.
Option Hooks and Callback Surfaces
The wrapper exposes upstream construction-time hooks as Python callables and protocol-style objects.
fetch: intercept or implement HTTP requests forcurland other network consumerslogger: receive upstreaminfo(...)/debug(...)eventstrace: receive structuredTraceEventtiming callbackscoverage: receive feature-hit notificationsdefense_in_depth: configure the upstream defense layer and optionally receiveSecurityViolationobjects
from collections.abc import Mapping
from just_bash import Bash, DefenseInDepthConfig, FetchRequest, FetchResult
class Logger:
def info(self, message: str, data: Mapping[str, object] | None = None) -> None:
print("INFO", message, data)
def debug(self, message: str, data: Mapping[str, object] | None = None) -> None:
print("DEBUG", message, data)
class Coverage:
def __init__(self) -> None:
self.hits: list[str] = []
def hit(self, feature: str) -> None:
self.hits.append(feature)
coverage = Coverage()
trace_events = []
violations = []
def fetch(request: FetchRequest) -> FetchResult:
return FetchResult(
status=200,
status_text="OK",
headers={"content-type": "text/plain"},
body="hello from fetch\n",
url=request.url,
)
with Bash(
logger=Logger(),
trace=trace_events.append,
coverage=coverage,
fetch=fetch,
javascript=True,
defense_in_depth=DefenseInDepthConfig(enabled=True, audit_mode=True, on_violation=violations.append),
) as bash:
print(bash.exec("curl -s https://example.com").stdout, end="")
bash.exec("find . -maxdepth 1 -type f")
See examples/option_hooks.py for a runnable end-to-end example.
Optional Capabilities
Network Access
Network access is disabled by default. Enable it with network=....
from just_bash import Bash
with Bash(
network={
"allowedUrlPrefixes": [
"http://example.com",
"https://api.github.com/repos/vercel-labs/",
],
}
) as bash:
result = bash.exec("curl -s http://example.com | html-to-markdown | head -n 12")
print(result.stdout, end="")
Like upstream just-bash, curl only exists when network access is configured. The repository example examples/network_access.py demonstrates allow-listed methods and header transforms using a local HTTP fixture, so it stays smoke-testable without depending on the public internet.
Python Support
The Python wrapper passes python=True through to upstream just-bash.
from just_bash import Bash
with Bash(python=True) as bash:
result = bash.exec('python -c "print(sum([2, 3, 5]))"')
print(result.stdout, end="")
JavaScript Support
The Python wrapper passes javascript=True or JavaScriptConfig(...) through to upstream just-bash.
from just_bash import Bash, JavaScriptConfig
with Bash(javascript=JavaScriptConfig(bootstrap="globalThis.prefix = 'bootstrapped';")) as bash:
result = bash.exec("js-exec -c 'console.log(globalThis.prefix + \":\" + (2 + 3))'")
print(result.stdout, end="")
See examples/configuration_and_runtimes.py for a runnable end-to-end example.
Broader Upstream Exports
The wrapper now also exposes the main parser / transform / sandbox / security helper surfaces that map cleanly into Python.
Command registry helpers and parser helpers
from just_bash import get_command_names, parse, serialize
script = "echo hello | grep h"
ast = parse(script)
print("echo" in get_command_names())
print(ast["type"])
print(serialize(ast))
Transform pipeline helpers
from datetime import UTC, datetime
from just_bash import Bash, BashTransformPipeline, CommandCollectorPlugin, TeePlugin
pipeline = (
BashTransformPipeline()
.use(TeePlugin(output_dir="/tmp/logs", timestamp=datetime(2024, 1, 15, 10, 30, 45, 123000, tzinfo=UTC)))
.use(CommandCollectorPlugin())
)
result = pipeline.transform("echo hello | grep h")
print(result.metadata)
with Bash() as bash:
bash.register_transform_plugin(CommandCollectorPlugin())
transformed = bash.transform("echo hello | grep h")
exec_result = bash.exec("echo hello | grep h")
print(transformed.metadata)
print(exec_result.metadata)
Sandbox helpers
from just_bash import Sandbox, SandboxOptions
with Sandbox.create(SandboxOptions(cwd="/app")) as sandbox:
sandbox.write_files({"/app/hello.txt": "hello from sandbox\n"})
command = sandbox.run_command("cat /app/hello.txt")
print(command.stdout(), end="")
Async code can use AsyncSandbox with the same high-level shape.
Security helpers
from just_bash import SecurityViolation, SecurityViolationLogger
logger = SecurityViolationLogger(include_stack_traces=False)
logger.record(
SecurityViolation(
timestamp=1,
type="eval",
message="blocked eval",
path="globalThis.eval",
)
)
print(logger.get_summary())
Examples
The repo includes a Python examples/ directory that mirrors the spirit of the vendored upstream examples and README. These examples are smoke-tested from the repo root so they stay aligned with the shipped public API:
| File | What it shows |
|---|---|
examples/quickstart_sync.py |
Basic synchronous usage, shell-state reset semantics, shared filesystem state, and bash.fs helpers |
examples/quickstart_async.py |
Native-async usage with AsyncBash and async filesystem helpers |
examples/custom_commands_sync.py |
A Python port of the upstream custom-command showcase |
examples/custom_commands_async.py |
Async custom commands with nested async exec |
examples/configuration_and_runtimes.py |
Session config, per-exec overrides, replace_env, Python, and JavaScript runtimes |
examples/filesystem_surfaces.py |
FileInit, LazyFile, FsStat, DirentEntry, and the session-bound filesystem API |
examples/network_access.py |
Allow-listed network access, method policy, and header transforms via a local HTTP fixture |
examples/option_hooks.py |
Python callback surfaces for fetch, logger, trace, coverage, and defense_in_depth |
examples/parser_and_command_registry.py |
Command-name helpers plus standalone parse(...) / serialize(...) |
examples/transforms.py |
Standalone transform pipelines and session-integrated transform registration |
examples/sandbox.py |
Upstream-style sandbox helpers, detached commands, and file IO |
examples/security_helpers.py |
Security violation logging helpers |
See examples/README.md for run instructions.
Result Handling
exec() returns an ExecResult with:
stdout: strstderr: strexit_code: intok: boolcheck()/check_returncode()
Example:
from just_bash import Bash
with Bash() as bash:
result = bash.exec("false")
if not result.ok:
print(result.exit_code)
print(result.stderr)
Backend Selection
By default the package uses its vendored just-bash runtime and resolves Node.js in this order:
node_command=passed toBash(...)orAsyncBash(...)JUST_BASH_NODE- the first-party bundled Node provider installed by
just-py-bash[node] - a system
nodeonPATH
When javascript=True or JavaScriptConfig(...) is enabled, the resolved Node.js runtime must be at least 22.6 because upstream just-bash's js-exec worker depends on node:module.stripTypeScriptTypes. If the resolved runtime is too old, the wrapper raises UnsupportedRuntimeConfigurationError before opening the session and suggests upgrading Node, installing just-py-bash[node], or overriding node_command=.
To point at a different just-bash backend artifact, set:
JUST_BASH_JS_ENTRYJUST_BASH_PACKAGE_JSON- optionally
JUST_BASH_NODE
If you provide only js_entry= or JUST_BASH_JS_ENTRY, the wrapper will try to infer the matching package.json by walking parent directories. That works for both dist/index.js and dist/bundle/index.js, but you can still pass package_json= / JUST_BASH_PACKAGE_JSON explicitly when you want to be precise.
CLI Launchers
The Python package now ships thin launchers over the upstream CLI assets:
just-py-bash→ delegates to upstreamjust-bashjust-py-bash-shell→ delegates to upstreamjust-bash-shell
These launchers forward argv, stdin, stdout, stderr, and the final exit code directly to the upstream CLI implementation. The Python package keeps Python-specific binary names, but CLI semantics come from upstream just-bash rather than a separate Python reimplementation.
Examples:
just-py-bash -c 'echo hello'
echo 'pwd' | just-py-bash
just-py-bash ./script.sh
just-py-bash --json -c 'echo hello'
just-py-bash-shell --cwd /
Scope Compared to Upstream TypeScript API
The wrapper now covers the main upstream session API, filesystem config and session-fs surfaces, option hooks, command-name helpers, standalone parser/serializer helpers, the built-in transform pipeline/plugin surfaces (BashTransformPipeline, CommandCollectorPlugin, TeePlugin), upstream-style sandbox/security helper utilities, and thin CLI delegation via just-py-bash / just-py-bash-shell.
What it still does not expose is the live TypeScript-side filesystem adapter interface for plugging arbitrary Python filesystem implementations directly into upstream just-bash. What it now does expose is the full vendored session-facing filesystem surface on bash.fs / async_bash.fs, including append_text / append_bytes, lstat, readdir_with_file_types, resolve_path, get_all_paths, symlink, link, and utimes, alongside the existing text/bytes helpers and core session operations. If you need to implement a new low-level filesystem backend in TypeScript, use upstream just-bash. If you want the Pythonic session-oriented shell API plus the portable parser / transform / sandbox / security helper surfaces described above, use just-py-bash.
Contributing
See the repo README for development setup, Makefile recipes, conformance testing, and release flow.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file just_py_bash-2.14.2.post1.tar.gz.
File metadata
- Download URL: just_py_bash-2.14.2.post1.tar.gz
- Upload date:
- Size: 8.6 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61c73bad1b9d5cbb9fefac2459aaedf29c2d25252cb4408f1a1e2e4af4afde49
|
|
| MD5 |
e222d05b021b7ceb5df335fcc5abbb69
|
|
| BLAKE2b-256 |
b5f4bce031478ccbb5dd772795d753df761023fbb132c13ee8fe00f87f9bfb5d
|
Provenance
The following attestation bundles were made for just_py_bash-2.14.2.post1.tar.gz:
Publisher:
release.yml on nathan-gage/just-py-bash
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
just_py_bash-2.14.2.post1.tar.gz -
Subject digest:
61c73bad1b9d5cbb9fefac2459aaedf29c2d25252cb4408f1a1e2e4af4afde49 - Sigstore transparency entry: 1300501474
- Sigstore integration time:
-
Permalink:
nathan-gage/just-py-bash@c30398b7b306f955a8bc5c616361105defa83f9f -
Branch / Tag:
refs/tags/v2.14.2.post1 - Owner: https://github.com/nathan-gage
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c30398b7b306f955a8bc5c616361105defa83f9f -
Trigger Event:
push
-
Statement type:
File details
Details for the file just_py_bash-2.14.2.post1-py3-none-any.whl.
File metadata
- Download URL: just_py_bash-2.14.2.post1-py3-none-any.whl
- Upload date:
- Size: 8.8 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
540da92113b5dc07f62bd0e74d4ec4f383d7a9da3152c6c404a83da240ffdb7e
|
|
| MD5 |
d9a86252d02ea9f44ac1220fc686f6c1
|
|
| BLAKE2b-256 |
8f9672088dc7b8a908678c542b8de6afaeb988ec2cc400c8e3085445044d455d
|
Provenance
The following attestation bundles were made for just_py_bash-2.14.2.post1-py3-none-any.whl:
Publisher:
release.yml on nathan-gage/just-py-bash
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
just_py_bash-2.14.2.post1-py3-none-any.whl -
Subject digest:
540da92113b5dc07f62bd0e74d4ec4f383d7a9da3152c6c404a83da240ffdb7e - Sigstore transparency entry: 1300501537
- Sigstore integration time:
-
Permalink:
nathan-gage/just-py-bash@c30398b7b306f955a8bc5c616361105defa83f9f -
Branch / Tag:
refs/tags/v2.14.2.post1 - Owner: https://github.com/nathan-gage
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c30398b7b306f955a8bc5c616361105defa83f9f -
Trigger Event:
push
-
Statement type: