Durable process substrate with named sessions shared by humans and agents.
⚠️ Pre-1.0 — core JOB / CRON flows,.cuescripts, real:fgPTY attach, client target resolution, and the official command set are implemented. Public contracts may still change before 1.0. Agent runtime concerns live above cue-shell.
cue-shell (cue) is a terminal-native runtime for durable async processes. It is not a traditional shell — it's a structured environment where sessions, jobs, scopes, chains, and crons are first-class primitives.
- Three-layer architecture: Process substrate (
cueddaemon) → Core model → Frontends (TUI/MCP/API) - Named shared sessions: humans and agents attach to one persistent job/scope context instead of maintaining separate terminal state
- Primary interaction modes: JOB ⚡ · CRON ⏰ — switch with
Shift+Tab :prefix commands: Vim-style builtin access (:run,:kill,:jobs,:cron, ...).cuefile scripts:cue run <file.cue>submits oneR<n>script with fail-fast execution- Shared foreground PTY attach:
:fg J<n>claims the single controller lease;:watch J<n>observes the same terminal read-only - Display tabs with clean semantics:
:out J<n>snapshots stdout,:tail J<n>follows live stdout,:err J<n>opens stderr - Scope persistence: Environment snapshots with delta storage and lifecycle management
- Chain syntax:
->serial ·~>ignore-failure ·|||parallel ·|?|any-success;&&/||stay inside one job - Daemon durability: named sessions, job history, cron definitions, and safe scope cursors survive client disconnects and daemon restarts
┌─────────────────────────────────────────┐
│ L3 Frontend: TUI / MCP / REST API │
├─────────────────────────────────────────┤
│ L2 Core model (cue-core) │
│ Session · Job · Scope · Chain · Cron │
├─────────────────────────────────────────┤
│ L1 Process substrate (cued daemon) │
│ Unix socket · SQLite · Process mgmt │
└─────────────────────────────────────────┘
crates/
├── cue-core/ — Core types and logic: Session, Job, Scope, Chain, Cron
├── cue-client/ — Client connection stack and `cue-client` CLI for session/target/run commands
├── cue-daemon/ — Background daemon library plus `cue-daemon` / `cued` CLIs
├── cue-tui/ — Interactive TUI frontend plus `cue-tui` CLI
├── cue-cli/ — `cue` aggregator entrypoint for explicit namespaces and extensions
# Install the official command set from PyPI:
# `cue`, `cue-client`, `cue-tui`, `cue-daemon`, and `cued`.
uv tool install cue-shell# Prerequisites: Rust 1.95+, just
# Build
cargo build
# Start daemon in foreground
cued -f
# Show the top-level aggregator help
cargo run -p cue-cli --bin cue --
# Start TUI (auto-connect / auto-reconnect)
cargo run -p cue-tui --bin cue-tui
# or through the aggregator
cargo run -p cue-cli --bin cue -- tui
# Create, inspect, and share a durable named session
cargo run -p cue-cli --bin cue -- session create dev
cargo run -p cue-cli --bin cue -- session list
cargo run -p cue-cli --bin cue -- tui --session dev
# Restart the daemon directly
cargo run -p cue-daemon --bin cue-daemon -- restart
# `cued` remains as a daemon alias
cargo run -p cue-daemon --bin cued -- restart
# Restart from inside the TUI
:restart
# Run checks
just check
# Run tests
just test
# Full CI locally
just ci
# Install pre-commit hooks
just pre-commit-installSee docs/design/README.md for the design index:
- Design overview — Three-layer architecture, crates, primitives, IPC summary
- conceptual-model.md — Jobs/scopes indexing, sequential composition, atomic tool surface
- commands-and-modes.md — Command reference, mode system,
:cronsyntax - cue-script.md —
.cuefile script contract forcue run <file.cue>
cue-shell uses split config files in the platform config dir:
client.toml— client-side transport/profile selection used bycue-client,cue-tui, andcue client ...daemon.toml— daemon-side runtime defaults used bycue-daemon/cued
Named sessions are daemon-owned process workspaces. Creating one captures the current cwd/environment scope; another human or agent client can attach by name and sees the same owned jobs, crons, output events, and subsequent scope cursor. Closing a TUI or client does not delete the session or stop its jobs.
cue session create dev
cue session list
cue tui --session dev
CUE_SESSION=dev cue run examples/hello.cue
Finished sessions can be hidden from the everyday list without deleting their state:
cue session archive old-dev
cue session list --archived
cue session list --all --json
cue session restore old-dev
Archive is deliberately reversible: the session identity, scope cursor, job
history, and retained terminal history remain intact. cue session list shows
active sessions by default, and archived sessions cannot be attached until
restored. The daemon refuses to archive a session that still has connected
clients, non-terminal jobs, pending script/chain work, or an owned cron; detach
clients and explicitly finish/cancel/remove those blockers first. There is no
force-archive or deletion path. Archive-aware clients require the daemon's
session-archive IPC capability and fail locally with an upgrade/restart hint
before sending an unsupported request to an older daemon.
Scopes containing credential-like environment values are deliberately not
written to SQLite. After a daemon restart such a session reports
needs_refresh; recover it explicitly from a trusted process environment with
cue tui --session dev --session-refresh or
CUE_SESSION=dev cue run examples/hello.cue --session-refresh. Ordinary
reconnects never replace an already-ready shared scope.
Foreground PTY jobs can be shared without sharing write access:
:fg J1 # attach and claim the controller lease
:watch J1 # attach as a read-only observer
cue fg watch J1 --session dev # persistent non-interactive observer
Every attachment receives the current terminal snapshot and subsequent live
output. Exactly one attached client may send keys, paste, resize, or use
:send against that PTY. In the fullscreen terminal view, Ctrl+] releases
control (or claims it when free), Ctrl+Z detaches, and Ctrl+Y copies the
visible terminal. Releasing control keeps the client attached as an observer;
disconnecting releases its controller lease without stopping the job.
cue fg watch keeps its daemon connection open until the PTY job exits. Its
default stdout is the exact initial terminal snapshot followed by matching live
PTY bytes, so it can be redirected without text conversion; lifecycle notices
go to stderr. Add --jsonl for snapshot, output, control_changed, and
exited records (binary fields are base64), or --session-refresh when a
selected named session explicitly needs recovery after a daemon restart.
Scripts are .cue files executed through the client CLI, with a top-level shortcut retained by the aggregator:
cue-client run examples/hello.cue
cue client run examples/hello.cue
cue run examples/hello.cue
A script run gets an id such as R12. Top-level items run in file order with
fail-fast semantics: if one item exits non-zero, later items are not submitted
and cue run exits with that code. Bare non-: items default to :run, while
explicit : commands remain available for builtins and :run(...) mode params.
Output still belongs canonically to jobs, while the daemon delivers script job
output and terminal script status directly to the cue run process so
stdout/stderr and the final exit code do not depend on event-channel
subscriptions.
Interactive JOB multiline input is not a script entry point; put multi-item
workflows in a .cue file. See docs/design/cue-script.md
and examples/.
cue-client and cue-tui default to a local Unix socket profile, so local users do not need any config for the current flow. The top-level cue command is an explicit aggregator: bare cue prints help, cue session ... forwards to cue-client session ..., cue client ... forwards to cue-client ..., cue tui ... forwards to cue-tui, and cue daemon ... forwards to cue-daemon. Target/profile commands are intentionally namespaced under the client surface; use cue-client target ... or cue client target ... rather than cue target ....
To make the split explicit:
[transport]
default_profile = "local"
[transport.profiles.local]
transport = "unix"
# socket = "/custom/path/to/cued.sock"
[transport.profiles.remote-dev]
transport = "ssh"
destination = "user@example.com"
gateway_command = "cued gateway --stdio"
start_command = "cued start"The local profile name is reserved for Unix socket transport; use another
profile name for SSH targets.
Phase 1 uses the system OpenSSH client and runs the configured gateway command
over SSH, so the client speaks the same IPC through cued gateway --stdio.
Remote daemon startup still stays explicit: cue will not run
start_command for you. cue-client owns client-side transport parsing and
resolution, so CLI/TUI frontends share the same client.toml behavior.
When auto_detect_ssh is enabled, cue-shell adds implicit SSH profiles from
~/.ssh/config. Additional cluster inventory can be configured generically
without hardcoding scheduler-specific names in cue-shell:
[transport.discovery]
# Values are host lists separated by comma, semicolon, or whitespace.
env_hosts = ["CLUSTER_HOSTS"]
# Values are endpoint lists; cue-shell extracts hosts from host:port or URI values.
env_endpoints = ["CLUSTER_ENDPOINTS"]
# Values point to files containing one host per line or host plus extra columns.
env_hostfiles = ["CLUSTER_HOSTFILE"]
# Values use bracket range syntax such as gpu-[01-03,08].
env_bracket_ranges = ["CLUSTER_NODELIST"]Site-specific schedulers should be wired through this config or through external
cue-* extensions rather than hardcoded in the cue-shell core. Other frontends,
including Pi cue integrations, should use the cue-client resolver if they need to honor client.toml:
cue-client target resolve --json
cue client target resolve --json
cue-client target list --json
Direct daemon/socket integrations only see the server side and will not apply client profile selection by themselves.
cue can also dispatch external CLI extensions from client.toml after checking aggregator namespaces and direct shortcuts:
[extensions.commands.foo]
program = "cue-foo"
description = "Foo extension"program is the executable path/name; extension arguments come from the cue foo ...
invocation. Then cue foo arg runs cue-foo arg. Aggregator namespaces and shortcuts such as client, tui, daemon, run, target, help, and version take precedence, and extension names must be kebab-case without colliding with built-in or first-party subcommands. Optional PATH lookup for unknown cue-<name> binaries can be enabled explicitly:
[extensions]
path_lookup = trueOn first startup, cued creates ~/.config/cue-shell/daemon.toml with the built-in guardrails.
daemon.toml can block unsafe commands or command arguments and attach remediation hints.
Block rules run before advisory warnings, so a command can both warn generally
and fail fast for specific arguments.
[block.versioned_commands]
python = "Use script_run/script_eval or uv run python ...; direct Python launchers are blocked."
[block.commands]
sh = "Avoid shell wrappers. Use cue-shell direct-exec, cwd=..., or cue operators."
[block.commands.git]
"--no-verify" = "Run the commit normally; if hooks fail, inspect and fix the hook/check."
[block.commands.npm]
"--force" = "Use the lockfile and normal install path."
[warn.commands]
cd = "Prefer cwd=... over cd in command strings."Matching is literal, not glob or regex based:
[block.commands] sh = "..."blocks a command whoseargv[0]basename is exactlysh. It matchesshand/bin/sh; it does not matchzsh,/bin/zsh, orshellcheck.[block.versioned_commands] python = "..."blocks command basenamespython,python2,python3,python3.12, etc.; it does not matchpython-config.- The generated default config uses
[block.versioned_commands] python = "..."to block directpython,python3, and versioned launchers such aspython3.12, so Python execution goes throughscript_run/script_evalor explicituv run python .... - Each
[block.commands.<name>]entry maps one blocked argument pattern to its remediation hint. The command name is also matched by exactargv[0]basename. - Argument patterns are checked against each argv token independently, not against the joined command line.
"--no-verify"matches an argument token--no-verify; it also matches--no-verify=...via the--flag=valueconvention. [warn.commands]maps an exact command basename to an advisory hint.
daemon.toml can cap persisted job/script history:
[retention]
max_job_history = 200
max_script_runs = 100It can also enable a runtime wrapper such as rtk. Wrapping is allowlist-only:
commands not listed under [wrapper.allowlist] are never wrapped, and an empty
allowlist wraps nothing.
[wrapper]
enabled = true
binary = "rtk"
[wrapper.allowlist]
commands = ["cargo", "git", "pnpm", "node"]:wrap on/off/status overrides only the session-level enablement. Per-command
mode params such as :run(wrapper=false) cargo test and cron params such as
:cron(wrapper=true) every 5m cargo test override enablement for that
invocation, but still must match the allowlist.
daemon.toml configures the overlay workspace sandbox used by
:run(sandbox=overlay):
[sandbox]
# Root under which each sandboxed job gets its own <root>/<job-id>/{upper,work}.
# Defaults to shared memory so writes stay off disk; point it at a disk-backed
# path if /dev/shm is too small for your builds.
default_upper_root = "/dev/shm/cue-shell-sandbox"
# Refuse to start a sandbox when the upper-root filesystem has less than this
# fraction free (0.0 disables the guard). Protects /dev/shm from runaway writes.
min_free_ratio = 0.1Each sandboxed job gets an independent per-job upper/work pair, so concurrent
jobs never share an overlay layer, and the writes are discarded when the job
finishes. An explicit :run(sandbox=overlay, sandbox.upper=<dir>) treats <dir>
as an upper root (the daemon still appends /<job-id>/{upper,work}), while
sandbox.upper=tmpfs forces a fresh in-memory upper for that job. Overlay
sandboxing is a workspace view, not a security boundary — it does not isolate
absolute paths outside the working tree, network access, process credentials, or
environment variables. See docs/design/sandbox-threat-model.md for the full
trust model.
Typical remote flow:
# Step 1: start the remote daemon explicitly
ssh user@example.com "cued start"
# Step 2: connect with the SSH transport profile
cueIf the remote daemon is not running (or its socket is missing), cue starts the
TUI offline and keeps retrying the configured gateway. cue run <file> still
fails immediately because file-script execution needs a live daemon.
| Component | Status |
|---|---|
| Design docs | ✅ Active |
| Cargo workspace | ✅ Multi-crate workspace |
| CI/CD | ✅ Tests, package smokes, PyPI/GitHub release path |
| cue-core | ✅ Core types / IPC / parser in place |
| cue-client | ✅ Named sessions / transport profiles / target JSON / script runner |
| cue-daemon | ✅ Durable sessions / jobs / crons / scopes / PTY attach |
| cue-tui | ✅ Session attach / interactive job+cron frontend / reconnect view |
| cue-cli | ✅ Aggregator / extension dispatch / package feature gates for first-party commands |
Licensed under the MIT License.