Scripting & Automation¶
contree-cli is designed for scripting. Exit codes propagate, output formats are machine-readable, and shebang mode lets you write executable sandbox scripts.
Shebang scripts¶
Note
Shebang scripts are a CLI-only feature. They are not available in the interactive shell.
Any file with a contree run -I shebang runs inside a sandbox:
#!/usr/bin/env -S contree run -I
echo "Hello from a ConTree sandbox"
uname -a
Save it, chmod +x, and run it directly:
chmod +x hello.sh
./hello.sh
The -I (interpreter) flag reads the script, strips the shebang line, and
sends the body as stdin to /bin/sh -s inside the sandbox.
Combining flags¶
Shebang flags stack. A disposable run with a 10-second timeout:
#!/usr/bin/env -S contree run -I -D -t 10
apt-get update -qq
apt-get install -y curl
curl https://example.com
Since -D is set, the session image is not advanced – the script runs in
a throwaway sandbox.
Passing arguments¶
Extra arguments after the script name are forwarded to the shell:
#!/usr/bin/env -S contree run -I
echo "arg1=$1 arg2=$2"
./script.sh foo bar
# arg1=foo arg2=bar
Note
The -S flag on /usr/bin/env is required because the contree entry point
is a Python script. Without -S, the kernel sees a nested shebang
(script -> script -> binary) and returns ENOEXEC. Using /usr/bin/env -S
(a real binary) splits the argument string and avoids this.
Execution modes¶
Direct command¶
The default mode. Each positional argument becomes a separate argv entry:
contree run uname -a
uname -a
Shell mode¶
-s joins all arguments into a single shell expression:
contree run -s -- 'echo hello && ls /'
echo hello && ls /
Bare commands in the shell always use shell mode.
Useful when you need pipes, redirects, or && chains.
Piped stdin¶
Note
Piped stdin is a CLI-only feature. It is not available in the interactive shell.
When stdin is not a TTY, it is read, base64-encoded, and sent to the sandbox:
echo 'uname -a' | contree run /bin/sh
cat deploy.sh | contree run /bin/sh
Detached mode¶
-d spawns the operation and exits immediately, printing the operation UUID:
contree run -d -- long-running-task
Check on it later:
contree show UUID
contree run -d -- long-running-task
contree show UUID
Flags like -d require the explicit contree run prefix.
Exit codes¶
Note
Exit code propagation is a CLI-only feature useful for scripting. The interactive shell does not expose sandbox exit codes.
The sandbox exit code propagates to the CLI process:
contree run -- /bin/sh -c 'exit 42'
echo $? # 42
This means contree run works naturally in if, &&, ||, and set -e
scripts:
set -e
contree run -- make test # script aborts if tests fail
contree run -- make install
If the operation fails at the platform level (timeout, cancelled), the CLI exits with code 1.
Environment variables¶
Pass environment variables into the sandbox with -e:
contree run -e DEBUG=1 -e DB_HOST=postgres -- ./app
contree run -e DEBUG=1 -e DB_HOST=postgres -- ./app
Flags like -e require the explicit contree run prefix.
The flag is repeatable. Format is KEY=VALUE.
Output truncation¶
By default, stdout/stderr is capped at 64 KiB in the API response. Override
with -T:
contree run -T 1048576 -- ./generate-big-output.sh
contree run -T 1048576 -- ./generate-big-output.sh
Monitor operations¶
List running and recent operations:
contree ps # active operations only
contree ps -a # all (including completed)
contree ps -q # UUIDs only, one per line
contree ps
contree ps -a
contree ps -q
Show the full result of a specific operation:
contree show UUID
contree show UUID
Cancel an operation:
contree kill UUID
contree kill --all
contree kill UUID
contree kill --all
Fan-out + wait¶
When several independent steps can run at the same time, spawn each
one detached and join them with contree op wait (alias contree operation wait). The wait command polls the API and prints one row
per operation as soon as it reaches a terminal status, with columns
uuid, status, exit_code, timed_out, duration, and any other
scalar field the API returns. The status column is the server’s
verdict (did the API run the job?) and is reported verbatim; the
sandbox process’s own exit code is in the separate exit_code column.
The CLI exit status is 1 when any op finished non-SUCCESS, or the
actual exit_code when a SUCCESS op exited non-zero, so the wait
still composes naturally with &&.
Important
op wait is a pure observer — it polls completion status but
does not touch local session state. That makes the pattern most
natural with --disposable (no image to track). For non-disposable
fan-out, the result images live only on the server; the
detached-<op-uuid> branches created at spawn time still point at
the starting image and never get moved. See the non-disposable
recovery example below.
The preferred shape — disposable runs, parallel independent checks.
The global -f json must come BEFORE the subcommand so that jq
gets JSON; the default run -d formatter is plain.
# Three parallel test suites, results discarded after the runs
A=$(contree -f json run -d --disposable -- pytest tests/a | jq -r .uuid)
B=$(contree -f json run -d --disposable -- pytest tests/b | jq -r .uuid)
C=$(contree -f json run -d --disposable -- pytest tests/c | jq -r .uuid)
# Block until each one finishes (or 60 s elapses, whichever comes first)
contree op wait "$A" "$B" "$C"
# Inspect stdout/stderr per leg
contree op show "$A" "$B" "$C"
Non-disposable fan-out works too, but you have to recover the result
images yourself — op wait will not bind them into the session:
A=$(contree -f json run -d -- apt-get install -y curl | jq -r .uuid)
B=$(contree -f json run -d -- apt-get install -y wget | jq -r .uuid)
contree op wait "$A" "$B"
# Pull the winning leg's image out of the operation result and
# attach it to the active session.
IMG_A=$(contree -f json op show "$A" | jq -r .image)
contree use "$IMG_A"
# Or tag it for reuse later.
contree tag "$IMG_A" feature/curl-tools
After fan-out + wait the session retains a detached-<op-uuid>
branch per spawn. They all point at the image that existed when the
fan-out started, so they are mostly cosmetic — feel free to delete
them with contree session branch --prune when you no longer need
them.
Useful flags:
--timeout SECONDS— cap on the wait (default 60). If the deadline hits before every operation reaches a terminal status,op waitemits one extra row per unfinished op withtimed_out=trueand the operation’s last observed status (e.g.EXECUTING), then exits with status1.--all— wait for every currently active operation in the project, not just the ones you passed.
# Block on every active op, up to 5 minutes
contree op wait --all --timeout 300
Warning
--all is project-scoped. If multiple agents or shell sessions
share the same project, op wait --all will block on every active
operation across all of them — not just the ones you launched. For
multi-agent or multi-shell setups prefer the explicit
op wait UUID1 UUID2 ... form with the UUIDs you actually own.
op wait exits non-zero whenever any operation finished with a
non-SUCCESS status (so it composes naturally with shell &&
chains), even when no --timeout was hit.
# Run fan-out + tests; bail if any leg failed
contree op wait "$A" "$B" "$C" && echo "all green" || echo "some failed"
Scripting patterns¶
Note
Shell piping and command substitution are CLI-only features. These patterns are not available in the interactive shell.
Combine -q with other tools:
# Show results of all active operations
contree ps -q | xargs -I {} contree show {}
# Kill all running operations
contree ps -q | xargs -I {} contree kill {}
# Launch detached, capture UUID
OP=$(contree run -d -- sleep 3600)
# ... do other work ...
contree show "$OP"
Output formats¶
Note
The --format flag is global and set at CLI launch time. In the interactive
shell, the format is fixed for the entire session and cannot be changed
mid-session.
Use -f / --format to control output. The flag is global and goes
before the subcommand:
default
: Table-like output optimized for human reading. Some commands (like
run) use a custom default that prints only stdout/stderr.
table
: Aligned columns with headers. Identical to default for most commands.
csv
: Comma-separated values with a header row. Useful for spreadsheet import
or cut/awk processing.
tsv
: Tab-separated values with a header row. Works well with column -t.
json
: One JSON object per line (JSONL/NDJSON). Each output row is a separate
JSON object. Suitable for jq processing.
json-pretty
: All rows collected into a single pretty-printed JSON array. Output is
flushed at the end.
Examples¶
# Pipe JSON to jq
contree -f json ps | jq '.uuid'
# CSV for scripting
contree -f csv images --tagged > images.csv
# Tab-separated for column alignment
contree -f tsv ps | column -t
# Get image UUID from tag
contree -f json images --prefix=ubuntu | jq -r '.uuid'
Streaming behavior¶
json and json-pretty formatters support streaming output from
commands like run and show – stdout/stderr are included in the
JSON payload.
csv, tsv, and table formatters do not include stdout/stderr
from sandbox execution. Use default or json formats to see
sandbox output.
Session management in scripts¶
Note
Script-level session management with eval and CONTREE_SESSION is a
CLI-only pattern. The interactive shell manages sessions automatically.
The eval $(contree use ...) pattern exports the session key into your
shell. In scripts, set CONTREE_SESSION explicitly to control which
session you operate on:
#!/bin/bash
export CONTREE_SESSION=ci-build-$$
contree use tag:ubuntu:latest
contree run apt-get update -qq
contree run apt-get install -y build-essential
contree run --file ./src:/src make -C /src test
Using $$ (PID) or a fixed name gives you a predictable, isolated session
per script run.
You now know the full CLI. Next: Configuration & Profiles.