| name | zowex-ssh |
| description | Deploy and operate the zowex JSON-RPC server on a *remote* z/OS host over SSH, for end-user dataset/job/USS operations when that host has no working z/OSMF endpoint. Use when the user asks to "connect to <host> over ssh", "deploy zowex to <lpar>", or "use zowex on <lpar>" to work with a remote system's datasets, jobs, or USS files. Not for building/testing this repo's own native code — that uses `npm run z:rebuild` / `npm run z:test`. |
zowex over SSH — deploy and operate
This skill is an alternative to the zowe CLI (z/OSMF REST): it uses the zowex native backend driven over SSH stdio instead. Use it when the target z/OS system has SSH but no z/OSMF. It's for operating on a remote target host's datasets/jobs/USS — unrelated to building or testing this repo's own native code, which uses npm run z:rebuild / npm run z:test.
Two helpers, one command grammar. Pick the one for your platform — they take the same subcommands, flags, env vars, and config keys:
| platform | helper | invoke as |
|---|
| macOS / Linux | zx (bash) | .agents/skills/zowex-ssh/zx ds list "SYS1.*" |
| Windows | zx.ps1 (Windows PowerShell 5.1+) | .agents\skills\zowex-ssh\zx.ps1 ds list "SYS1.*" |
Everything below is written with the bash helper; on Windows substitute zx.ps1 (or install the zx.cmd shim — §0) and PowerShell variable syntax. Windows-specific behavior and gotchas are collected in §8 — read that section before your first call on Windows.
Everything below uses two shell variables you should set once per target:
ZX_HOST=user@host
ZX_DIR=/u/$USER/zowex
If the user gives only a hostname, ask for (or infer) the SSH user and a writable USS directory.
If you are an LLM/agent running this skill, always set your own unique ZX_STATE first — never rely on the default. All zx state (config, persistent-session pid, FIFOs, ControlMaster socket) lives under $ZX_STATE, which defaults to one shared directory per UID (/tmp/zx.$UID). That default is meant for a human's interactive shell. If an agent run uses it too, it collides with any persistent session the user has open — zx use/zx deploy overwrites their saved host config, zx start/zx stop/zx reset clobbers their session, and vice versa. It also lets two concurrent agent runs stomp on each other the same way.
export ZX_STATE=/tmp/zx-agent-$$.$UID
$env:ZX_STATE = "$env:TEMP\zx-agent-$PID" # same rule on Windows (default is %TEMP%\zx.%USERNAME%)
Do this at the very start of every session, before the first zx/$zx call — deploy included. Never call zx reset under a ZX_STATE a human or another agent might be using (see §7).
Local prereqs (bash): ssh, sftp, jq, base64, bash ≥3.2, and curl or wget. Run .agents/skills/zowex-ssh/zx check to verify. (jq is the only one not stock on macOS — if it's missing, ask the user to install it via their package manager, e.g. Homebrew on macOS, before continuing.)
Local prereqs (Windows): ssh + sftp (the Windows OpenSSH Client feature) and Windows PowerShell 5.1 or newer. No jq/base64/curl needed — zx.ps1 does JSON, base64, and downloads in-process. Run .agents\skills\zowex-ssh\zx.ps1 check to verify.
Remote prereqs: SSH login + a writable USS directory. The zowex binary is self-contained.
Bundle: zx deploy will auto-download the latest server.pax.Z from github.com/zowe/zowex/releases if it isn't found locally. Default save path is ~/.local/share/zx/server.pax.Z (%LOCALAPPDATA%\zx\server.pax.Z on Windows) — always user-writable, works whether the helper is run directly or via PATH. Downloads automatically without prompting. To pin a specific version or path, set ZX_PAX to it. Set GITHUB_TOKEN if the API is rate-limited on a shared corporate IP.
0 · Add zx to your PATH (optional but recommended)
Run once to symlink the helper into ~/.local/bin (created if absent):
.agents/skills/zowex-ssh/zx install
.agents/skills/zowex-ssh/zx install ~/bin
zx install prints an export PATH=... line if the target directory isn't already on your PATH — paste it into your shell profile (~/.zshrc, ~/.bashrc, etc.).
On Windows there are no symlink permissions to worry about — install writes a zx.cmd shim that launches the script with -ExecutionPolicy Bypass, so it works regardless of the machine's execution policy:
.agents\skills\zowex-ssh\zx.ps1 install # -> ~\.local\bin\zx.cmd (default)
.agents\skills\zowex-ssh\zx.ps1 install C:\tools # -> C:\tools\zx.cmd (custom dir)
It prints the $env:PATH / [Environment]::SetEnvironmentVariable(...) line to run if the directory isn't on PATH yet.
After that, every command below can be called as zx ... directly. To remove the symlink/shim: zx uninstall.
1 · One-time deploy
The easiest path — just run zx deploy:
zx deploy user@host [/remote/dir]
If server.pax.Z isn't present locally, zx deploy will:
- Hit the GitHub releases API to find the latest release asset (
*.pax.Z)
- Show you the filename and destination, then download it automatically
- Continue with the normal sftp → unpax → verify flow
The release asset may carry a version in its name (e.g. zowex-0.7.0-server.pax.Z); zx saves it as server.pax.Z at the ZX_PAX path.
Manual / pinned-version deploy — download the .pax.Z from github.com/zowe/zowex/releases, then:
ZX_PAX=/path/to/downloaded.pax.Z
zx deploy user@host [/remote/dir]
Low-level steps (if you need to do it by hand):
sftp -b - "$ZX_HOST" <<EOF
-mkdir $ZX_DIR
cd $ZX_DIR
put server.pax.Z
bye
EOF
ssh "$ZX_HOST" "cd $ZX_DIR && pax -rvf server.pax.Z && chmod +x zowex"
ZX_BIN=$ZX_DIR/zowex
ssh "$ZX_HOST" "$ZX_BIN --help"
If pax -rvf fails on the .Z, do uncompress server.pax.Z && pax -rvf server.pax.
The CLI also exposes tso / system / tool subcommands — run $ZX_BIN <cmd> --help if you need one of those interactively. Only a subset is exposed via JSON-RPC (see §3; tso is RPC-backed, most of system/tool is CLI-only). Console is special: on servers newer than v0.8.0 the console CLI group exists only in the separate APF-authorized zoweax binary, and the consoleCommand RPC (see §3) is how zowex server reaches it. This CLI-only gap isn't limited to those groups: data-set copy is CLI-only too (no copyDataset RPC — see §3's Datasets table). When in doubt whether a zowex data-set/job/uss verb has an RPC equivalent, check $ZX_BIN data-set --help against §3 rather than assuming parity.
2 · Talking to the server
zowex server reads JSON-RPC 2.0 requests on stdin and writes JSON responses on stdout, one object per line. Auth is whatever SSH gave you — there is no in-band handshake.
Wire behavior (verified against v0.6.0):
- The server emits one ready banner line first:
{"status":"ready","message":"...","data":{"version":"..."}}. Consume and discard it before reading responses.
- With the default worker pool, responses can arrive out of order (matched by
id). For scripted/sequential use, start with -w 1 to serialize. The zx helper does this.
- Unknown methods return
{"error":{"code":-32601,"message":"Unrecognized command <name>"}}.
- Result shape:
{"id":N,"result":{"success":true, ...},"jsonrpc":"2.0"}. Common result fields: data (b64 for readDataset/readFile/readSpool; plain text for unixCommand/tsoCommand), items (lists), jobId/jobName (submitJcl), etag (readDataset), returnedRows.
2a · The zx helper (preferred)
zx=.agents/skills/zowex-ssh/zx
$zx deploy user@host [/remote/dir]
$zx ds list "SYS1.*"
$zx job submit foo.jcl
$zx start
$zx job status JOB01234
$zx stop
On Windows, same sequence:
$zx = '.agents\skills\zowex-ssh\zx.ps1' # or install the zx.cmd shim onto PATH (§0)
& $zx deploy user@host /u/user/zowex
& $zx ds list "SYS1.*"
& $zx start # strongly recommended on Windows - see §8
& $zx job status JOB01234
& $zx stop
zx remembers host+bin in $ZX_STATE/config (config.json for zx.ps1; set by deploy or zx use <host> <bin>; overridable via ZX_HOST/ZX_BIN env). Every grouped command auto-detects whether a persistent session is live and falls back to a one-shot ssh if not.
Command groups (run zx help for the full list):
| group | subcommands |
|---|
zx ds | list <pat> · members <dsn> · read <dsn> · write <dsn> <file> · get/put [--binary] (member or whole PDS; default is text with EBCDIC conversion — pass --binary for byte-faithful transfer, e.g. load modules or tersed files) · create · delete · copy · rename |
zx job | list [<owner> [<prefix>]] · submit <file> · status <id> · spools <id> · spool <id> <n> · jcl <id> · cancel/delete/hold/release <id> |
zx uss | ls · get/put (sftp, binary-safe) · read/write (RPC, text) · rm · mkdir · mv · cp · chmod · chown · chtag · sh '<cmd>' |
zx tso | '<cmd>' |
zx system | apf · linklist · proclib · syslog (RPC) · parmlib · subsystems · symbol <s> (CLI) |
zx tool | amblist <dsn> --cs '<stmts>' · run <pgm> [opts] · search <dsn> <str> · dynalloc · dsect (all CLI) |
zx console | '<cmd>' [--cn <n>] [--timeout <s>] [--no-wait] (RPC , servers > v0.8.0; needs installed + APF-authorized on the host and ESM OPERCMDS grants for non-display commands) |
There is no zx group yet for the ESM certificate / key ring commands (zowex system cert ... / zowex system keyring ..., servers newer than v0.7.0) — drive them with zx rpc <method> (see §3's Certificates table) or CLI passthrough: ssh "$ZX_HOST" "$ZX_BIN system keyring list-rings $USER".
Output: grouped commands pretty-print by default (lists → one per line, b64 → decoded, status → key=value). Add -j/--json anywhere (before or after the group) for raw JSON: zx ds list "SYS1.*" -j or zx -j ds list "SYS1.*".
zx info shows config + whether a session is live. zx reset is a harder stop: kills the session, closes the ControlMaster socket, and removes the entire $ZX_STATE directory — useful when the state gets corrupted or you want a clean slate.
2b · One-shot
For a single call without the persistent session (note -w 1; first output line is the ready banner):
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"listJobs","params":{"owner":"*","prefix":"XYZ*"}}' \
| ssh "$ZX_HOST" "$ZX_BIN server -w 1" | sed 1d
2c · Isolated / parallel connections (second host without disturbing the first)
All zx state — config, persistent-session pid, FIFOs, and the SSH ControlMaster socket — lives
under $ZX_STATE, which defaults to one shared directory per UID (/tmp/zx.$UID). That means
zx deploy/zx use for a second host overwrites the first host's saved config, and — more
importantly — _live() (the check every grouped command uses to decide "route through the
persistent session or go one-shot") only checks whether a pid exists, not which host it's
for. If a persistent session is already live for host A and you need one-shot calls against host
B without touching it, exporting ZX_HOST/ZX_BIN alone is not enough — the dispatcher will
still shove your request down host A's session.
Fix: give the second host its own ZX_STATE so it never sees host A's pid file or socket:
export ZX_STATE=/tmp/zx-<label>.$UID
export ZX_HOST=user@hostB
zx deploy "$ZX_HOST" /remote/dir
zx ds list "HLQ.*"
If host B needs password auth and you can't leave an interactive prompt open, prime the
ControlMaster once with sshpass using the same ControlPath pattern zx uses, then let
zx reuse the socket silently for subsequent calls:
sshpass -p "$PASSWORD" ssh -o ControlMaster=auto -o ControlPath="$ZX_STATE/cm-%C" \
-o ControlPersist=30m -T "$ZX_HOST" exit
Do this under the same isolated $ZX_STATE you'll use for the real zx calls — the %C token
resolves to the same socket path either way, so zx's own ssh invocations find it already open
and skip the password prompt. Never run zx start/zx stop/zx reset in this workflow — those
only make sense for the single shared default state and reset will nuke whichever $ZX_STATE
is currently exported.
On Windows the same isolation rule applies — a separate $env:ZX_STATE gives you a separate
config, pid file, and named pipe (the pipe name is derived from the state directory), so two
ZX_STATEs never see each other's sessions. The ControlMaster/sshpass priming above does
not apply: Windows OpenSSH has no connection multiplexing. Instead give each host its own
ZX_STATE and run zx start per host — each gets its own long-lived ssh connection, and each
authenticates once (§8).
Envelope
{"jsonrpc":"2.0","method":"<name>","params":{...},"id":<int>}
id must be unique per request within a session.
- All
data fields (file/dataset content, JCL text) are base64.
- Responses are standard JSON-RPC 2.0:
{"jsonrpc":"2.0","id":N,"result":{...}} or {"...","error":{...}}.
3 · Method reference
Datasets
| method | params |
|---|
listDatasets | {"pattern":"HLQ.*"} |
listDsMembers | {"dsname":"HLQ.PDS"} |
readDataset | {"dsname":"HLQ.PS"} · optional "encoding":"ISO8859-1" |
writeDataset | {"dsname":"HLQ.PS","data":"<b64>"} · optional "encoding" |
createDataset | {"dsname":"HLQ.NEW","attributes":{"primary":10,"lrecl":80}} |
deleteDataset | {"dsname":"HLQ.OLD"} |
renameDataset | {"dsnameBefore":"A","dsnameAfter":"B"} |
No copyDataset RPC — at least as of server v0.6.0, copyDataset isn't wired over JSON-RPC (returns -32601 Unrecognized command) even though it exists as a CLI verb. zx ds copy calls the CLI directly instead (zowex data-set copy <src> <dst> [--ow|-r], via SSH passthrough — see §2's CLI-vs-RPC note). If you're issuing raw RPC via zx rpc or zx -j ds copy-style JSON, don't rely on copyDataset — shell out to the CLI form instead.
Jobs
| method | params |
|---|
listJobs | {} · optional "owner", "prefix" |
getJobStatus | {"jobId":"JOB01234"} |
listSpools | {"jobId":"JOB01234"} |
readSpool | {"jobId":"JOB01234","spoolId":2} |
getJcl | {"jobId":"JOB01234"} |
submitJcl | {"jcl":"<b64 of JCL text>"} |
submitJob | {"dsname":"HLQ.JCL(MBR)"} |
submitUss | {"fspath":"/path/job.jcl"} |
cancelJob / deleteJob / holdJob / releaseJob | {"jobId":"JOB01234"} |
USS
| method | params |
|---|
listFiles | {"fspath":"/u/x"} |
readFile | {"fspath":"/u/x/f"} · optional "encoding" |
writeFile | {"fspath":"/u/x/f","data":"<b64>"} · optional "encoding" |
createFile | {"fspath":"/u/x/f"} · add "isDir":true for mkdir |
deleteFile | {"fspath":"/u/x/f"} |
moveFile | {"source":"...","target":"..."} |
copyUss | {"srcFsPath":"...","dstFsPath":"..."} |
chmodFile | {"fspath":"...","mode":"755"} |
chownFile | {"fspath":"...","owner":"..."} |
chtagFile | {"fspath":"...","tag":"UTF-8"} |
unixCommand | {"commandText":"<shell cmd>"} — result.data is plain text |
TSO
| method | params |
|---|
tsoCommand | {"commandText":"<tso cmd>"} — result.data is plain text |
Console
Server version gate: consoleCommand exists only in servers newer than v0.8.0 (zowex PR #1110). On older servers it returns -32601 Unrecognized command.
| method | params |
|---|
consoleCommand | {"commandText":"<mvs cmd>","consoleName"?:"<name>","timeout"?:N,"wait"?:bool} — result.data is plain text |
The server spawns the APF-authorized zoweax binary per request (located next to zowex, on the server's PATH, or via ZOWEAX_PATH on the remote host) — the server itself never holds APF authorization. Prereqs on the host: zoweax installed and extattr +ap'd by a system programmer, and ESM OPERCMDS grants (MVS.* profiles, e.g. MVS.REPLY.* for WTOR replies) for anything beyond informational D ... displays. consoleName defaults to the SSH user's ID plus a digit suffix; console activation can be gated per user with MVS.MCSOPER.<name> profiles. Failures come back with actionable messages ("command not found" → zoweax not installed; "Not authorized" → not APF-authorized or ESM denied).
Certificates / key rings (ESM)
Server version gate: these methods exist only in servers newer than v0.7.0 (zowex PR #1079). On older servers they return -32601 Unrecognized command. The caller's SSH user needs the corresponding ESM IRR.DIGTCERT.* / RDATALIB authority — without it, calls fail with a SAF diagnostic (see below), not an auth prompt.
CLI equivalents live under zowex system cert ... and zowex system keyring ....
| method | params |
|---|
createKeyring | {"owner":"USER01","keyring":"RING02"} |
deleteKeyring | {"owner":"USER01","keyring":"RING02"} |
listRings | {"owner":"USER01"} · optional "keyring" to narrow — rings + connected certs |
countRing | {"owner":"USER01","keyring":"RING02"} — "*" counts all the owner's certs |
listCertificates | {"owner":"USER01","keyring":"RING02"} · optional "label", "usage", "labelOnly", "ownerOnly", "maxEntries" (default 10, 0 = all) — result.moreAvailable flags truncation |
showCertificate | {"owner":"USER01","keyring":"RING02","label":"CERT03"} — adds serial, validity dates, key size |
exportCertificate | {"owner":"USER01","keyring":"RING02","label":"CERT03"} · optional "format" (pem default / p12), "file" (server-side path; required for p12), "password" (p12) — result.data is b64 |
importCertificate | {"owner":"USER01","keyring":"RING02","label":"CERT03","usage":"PERSONAL","file":"/u/user01/c.p12","password":"..."} · optional "skipRefresh" — file is a server-side PKCS#12 path; usage is PERSONAL or CERTAUTH |
deleteCertificate | {"owner":"USER01","keyring":"RING02","label":"CERT03"} · or "database":true (omit keyring) to delete from the ESM DB · optional "skipRefresh" |
|
Conventions shared by these methods:
"*" as keyring is the ESM virtual key ring (all of the owner's certificates) — valid for list/count/show/export reads; rejected as a target for create/delete-ring/import/connect (use the database/fromDatabase booleans instead).
- Failures return
result.success:false with message, service, and a structured safReturns object (functionCode, safReturnCode, esmReturnCode, esmReasonCode); GSK (System SSL) failures add gskReturnCode. Non-fatal SAF warnings (rc 4) succeed with a warning string.
owner, keyring, and label are case-sensitive (userids normally uppercase).
unixCommand / tsoCommand are the escape hatches for anything not covered. MVS console commands have their own method on servers newer than v0.8.0 — consoleCommand (see the Console table above). On older servers, run the APF-authorized companion binary directly on the host: ssh "$ZX_HOST" "zoweax console issue ..." (zowex console issue only exists on pre-v0.8.x binaries and requires the old fat zoweax there too).
4 · zowe-CLI → zx mapping
When porting a workflow that used the zowe CLI:
zowe CLI | zx | underlying RPC method |
|---|
zowe files ls ds "PAT" | zx ds list "PAT" | listDatasets |
zowe files ls am "DSN" | zx ds members "DSN" | listDsMembers |
zowe files view ds "DSN" | zx ds read "DSN" | readDataset (b64) |
zowe files upload ftds f "DSN" | zx ds write "DSN" f | writeDataset |
zowe files delete ds "DSN" -f | zx ds delete "DSN" | deleteDataset |
zowe jobs submit lf f.jcl | zx job submit f.jcl | submitJcl (local file) |
zowe jobs submit ds "HLQ.JCL(MBR)" | zx job submit "HLQ.JCL(MBR)" | submitJob (dataset) |
zowe jobs submit uss "/path/job.jcl" | zx job submit /path/job.jcl | submitUss (USS path) |
zowe jobs view jsbj JOBID | zx job status JOBID | getJobStatus → .status/.retcode/.phaseName |
zowe jobs list sfbj JOBID | zx job spools JOBID | listSpools |
zowe jobs view sfbi JOBID N | zx job spool JOBID N | readSpool (b64) |
zowe jobs cancel job JOBID | zx job cancel JOBID | cancelJob |
| (AMBLIST batch job) |
Submit-then-poll pattern:
JID=$(zx job submit f.jcl | grep '^jobId=' | cut -d= -f2)
while [[ $(zx -j job status "$JID" | jq -r .result.status) == ACTIVE ]]; do sleep 2; done
zx job spools "$JID"
zx job spool "$JID" 2
5 · File transfer
| Command | What | Transport |
|---|
zx uss get <remote> [<local>] | download one USS file (binary-safe) | sftp |
zx uss put <local> <remote> | upload one USS file (binary-safe) | sftp |
zx ds get "<DSN(M)>" <file> | download one PDS member | RPC readDataset (b64) |
zx ds get "<DSN>" <localdir> | download all members of a PDS | loops listDsMembers + readDataset |
zx ds put <file> "<DSN(M)>" | upload one file as a member | RPC writeDataset (b64) |
zx ds put <localdir> "<DSN>" | upload directory → PDS (filename → MEMBER, ext stripped, upcased, 8-char) | loops writeDataset |
ds get|put go through base64-in-JSON, so use them for text members (REXX/JCL/parmlib/source). For load modules or anything large/binary, stage through USS:
zx uss sh 'cp -B "//'\''SYS1.LINKLIB(IEFBR14)'\''" /tmp/iefbr14.bin'
zx uss get /tmp/iefbr14.bin
For whole-PDS ds get, run zx start first — one persistent session is much faster than N one-shot SSH connects. ds put of a directory may hit zowex DEQ contention on rapid same-PDS writes; failures are reported per-member and the command exits non-zero if any failed.
6 · Troubleshooting
| Symptom | Likely cause / fix |
|---|
password prompt on every zx call | zx already multiplexes its own ssh calls per $ZX_STATE (30min ControlPersist), so this is usually only about manual/raw ssh calls outside zx. Prefer ssh-copy-id <host> (a key). If the user specifically wants a long-lived multiplexed session across all ssh tools to that host, the fix is a host-wide, persistent change to ~/.ssh/config (ControlMaster auto / ControlPath / ControlPersist) — it keeps an authenticated socket open for the persist duration, reusable by any local process as that user. That's a change outside this session's scope: confirm with the user and get their desired ControlPersist before editing ~/.ssh/config, don't add it unprompted. |
mkdir: ... EDC5134I Function not implemented on deploy | parent dir is an automount root — zx deploy now cds to the parent first to trigger the mount; if it still fails, the parent genuinely doesn't exist |
pax: FSUM7108 cannot open | wrong dir / no write perms — pick a different ZX_DIR |
zowex: FSUM7351 not found | ZX_BIN path wrong — re-run the find from step 1b |
EDC5129I No such file or directory on zowex --help | binary not tagged/executable — chmod +x $ZX_BIN; if it's a tag issue, chtag -b $ZX_BIN |
| server returns nothing then EOF | request wasn't newline-terminated, or JSON was malformed — zx rpc always appends \n |
CEE3501S module not found | LE runtime not in LIBPATH — prefix server start with export LIBPATH=$ZX_DIR/c/build-out:$LIBPATH; |
CEE3561S ... was not found in DLL CRTEQCXE | the target's Language Environment libc++ doesn't export a symbol the binary needs — a maintenance level problem, not just a z/OS release. Nothing to fix on the client side: the binary must be built with Open XL 2.1 (not 2.2) for a z/OS 2.5 target, or the target needs LE PTFs. See |
7 · Cleanup
zx stop
zx reset
ssh "$ZX_HOST" "rm -rf $ZX_DIR"
Only remove $ZX_DIR on the remote side if you created it for this session.
On Windows, zx.ps1 stop shuts down the session host and reaps its ssh child (tracked in
$ZX_STATE\ssh-pid), and zx.ps1 reset additionally wipes the state directory. There is no
ControlMaster socket to close.
8 · Windows notes (zx.ps1)
Behavior differences vs. the bash helper — everything else (subcommands, -j, ZX_HOST/ZX_BIN/ZX_STATE/ZX_PAX/ZX_TIMEOUT/GITHUB_TOKEN, method coverage, output shapes) is the same.
- Requires Windows PowerShell 5.1+ (the version that ships with Windows) or PowerShell 7. No
jq, base64, curl, or wget — JSON, base64, and the release download are done in-process.
- No connection multiplexing. Windows OpenSSH doesn't implement
ControlMaster/ControlPath, so every one-shot call is a new SSH connection (slower, and one auth per call with passwords). zx start is the stand-in: it launches a hidden-ish background PowerShell host that owns one long-lived zowex server -w 1 over ssh and answers one request per named-pipe connection. Every grouped command auto-routes through it when it's live, exactly like the bash version. Use zx start for anything more than a couple of calls, and always for a whole-PDS ds get/ds put.
- The host is started with
-NoNewWindow so it keeps your console: with password auth, ssh's prompt appears in your terminal during zx start (which waits up to 60s, or ZX_TIMEOUT if larger, for the ready banner). Key auth (ssh-keygen + append your public key to the host's ~/.ssh/authorized_keys) avoids the prompt entirely.
- The pipe name is derived from
$env:ZX_STATE, so per-agent/per-host isolation works the same way (§2c) and there's no socket-path length limit (the bash ControlPath too long problem doesn't exist here).
zx info prints the state dir, config, pid, and pipe name.
- Output goes through the PowerShell pipeline (
Write-Output), so zx ds list "X.*" | Select-String FOO, $rows = zx -j ds list "X.*" | ConvertFrom-Json, and Tee-Object all work. Errors and progress lines go to stderr. Caveat: PowerShell 5.1's > writes UTF-16 — use zx ds get / zx uss get for files, or Out-File -Encoding utf8.
- State/paths:
$env:ZX_STATE defaults to %TEMP%\zx.%USERNAME% and holds config.json, pid, ssh-pid, ready, , . defaults to .