terok User Guide¶
[!WARNING] This documentation was written by an AI agent and might be inaccurate.
Complete guide to installing, configuring, and using terok.
[!WARNING] Terok is in alpha development phase. It is under active development and until version 1.0.0 is released, APIs, internals, and security boundaries may change without deprecation notice. Not recommended for production deployment.
Table of Contents¶
- Installation
- Runtime Locations
- Global Configuration
- From Zero to First Run
- Authentication
- Headless Agent Runs (Unattended)
- Task Lifecycle Hooks
- Image Management
- Project Management
- GPU Passthrough
- Tips
- FAQ
Installation¶
Recommended: pipx¶
Alternative: pip¶
After install, the terok command is on $PATH. Run it with no arguments
on a TTY to launch the TUI; pass a subcommand to drive the CLI.
Global Flags¶
| Flag | Description |
|---|---|
--no-emoji |
Replace emojis with text labels (e.g. [gate] instead of emoji) |
--experimental |
Enable experimental features (e.g. web tasks, krun runtime) |
Subsystem Surfaces¶
terok exposes two surfaces over the sibling subsystems (terok-executor, terok-sandbox):
| Surface | Identity | Config | When to use |
|---|---|---|---|
terok <X> (incl. terok executor *, terok sandbox *, terok vault, terok ssh, terok gate) |
(project, task) for terok-native verbs; container id or project/task for forwarded per-container verbs |
terok's resolved cfg | Anything tied to your terok install |
Standalone terok-executor / terok-sandbox |
container id | sibling defaults / TEROK_CONFIG_FILE override |
Raw, no-terok-context operations (testing, scripting, custom orchestration) |
Every terok <X> invocation uses terok's configured state — same sandbox install, same vault, same auth, same image cache. For raw operation, install the sibling separately:
pipx install terok-executor # standalone executor
terok-executor show-config # raw cfg (no terok context)
TEROK_CONFIG_FILE=/path/to/cfg.yml terok-executor run claude . # explicit override
Identity forms on per-container verbs¶
terok executor stop (and future exec / logs / state / login) accept either form:
terok executor stop <container-id> # raw form
terok executor stop <project>/<task> # slash form — resolved via terok's task store
Same dual-form precedent as git push origin master vs origin/master. terok task <verb> p t and terok task <verb> p/t are accepted interchangeably for the same reason.
Diffing the effective config¶
To verify that terok executor * operates on terok's configured state:
terok executor show-config # cfg terok would pass down
terok-executor show-config # cfg standalone executor would read
diff <(terok executor show-config) <(terok-executor show-config)
For fields with a representation in the shared config.yml schema that sandbox/executor own, the two outputs match. Runtime-only ambient context (added by orchestrators) may differ.
Shell Completion¶
Tab completion is powered by argcomplete.
Recommended: auto-install to your shell's completion directory
# Auto-detect shell from $SHELL and install
terok completions install
# Or specify the shell explicitly
terok completions install --shell bash
Install locations (auto-loaded, no RC file edits needed):
| Shell | Path |
|---|---|
| bash | ~/.local/share/bash-completion/completions/terok |
| zsh | ~/.local/share/zsh/site-functions/_terok |
| fish | ~/.config/fish/completions/terok.fish |
Alternative: print raw completion script
terok completions bash # Print bash completion script to stdout
terok completions zsh # Print zsh completion script to stdout
terok completions fish # Print fish completion script to stdout
Run terok config paths to check whether completions are detected as installed.
Runtime Locations¶
Config/Projects¶
| Install Type | Path |
|---|---|
| Root | /etc/terok/projects |
| User | ~/.config/terok/projects |
| Override | TEROK_CONFIG_DIR=/path/to/config |
State (writable: tasks, build, gate)¶
| Install Type | Path |
|---|---|
| Root | /var/lib/terok |
| User | ${XDG_DATA_HOME:-~/.local/share}/terok |
| Override (whole tree) | TEROK_ROOT=/path/to/root or paths.root in config.yml |
| Override (terok core state only) | TEROK_STATE_DIR=/path/to/state |
Build/run output logs¶
Image builds and task launches stream their output to your terminal and persist it durably, so the diagnostic signal survives the terminal. The live output is byte-for-byte unchanged (colours and progress bars intact — the capture fronts the operation with a pseudo-terminal); the durable copy goes to one of two backends, chosen automatically:
| Host | Backend | Retrieve | Retention |
|---|---|---|---|
| systemd present | journald | journalctl -t terok TEROK_KIND=run (add TEROK_TASK=<id> / TEROK_PROJECT=<name> to filter) |
managed by journald |
| no systemd | timestamped files under …/terok/core/projects/<project>/logs/ (build-*.log, run-<task>-*.log) |
unlimited (never auto-pruned) |
Each operation prints a ↳ output … pointer to stderr on completion so the
location is discoverable. The file backend is deliberately unbounded; to cap
growth on a non-systemd host, point logrotate at the glob — see
examples/logrotate/terok.conf.
On systemd hosts nothing extra is needed — journald owns retention.
Global Configuration¶
Global config is merged from up to three layers (later layers override earlier ones, key by key):
/etc/terok/config.yml(system default)sys.prefix/etc/terok/config.yml(pip/venv installs)${XDG_CONFIG_HOME:-~/.config}/terok/config.yml(user override)
Setting TEROK_CONFIG_FILE=/path/to/config.yml disables layering and
uses only that file. terok config paths prints the merge order.
Example Config¶
Copy from examples/terok-config.yml:
Minimum Settings¶
Every project needs these fields in its project.yml:
project:
name: myproj
security_class: gatekeeping # default — or "online" for direct push
image:
base_image: docker.io/library/ubuntu:24.04
git:
upstream_url: https://github.com/yourorg/yourrepo.git # optional
git.upstream_url is optional. Projects without one start their task
containers with an empty workspace — useful for local experiments.
See Gate and upstream combinations.
Nice-to-Have Settings¶
These can be set in project.yml (per-project) or config.yml (global default):
git:
human_name: "Your Name"
human_email: "your@email.com"
default_branch: main
authorship: agent-human # or: human-agent, human, agent
Gate and upstream combinations¶
The host-side git gate (mirror) and the project's remote upstream_url
are independent knobs. Four combinations:
gate.enabled |
upstream_url |
Behaviour |
|---|---|---|
true (default) |
set | Host mirrors upstream; container clones from the mirror. Default. |
true |
absent | Host initialises a remoteless bare gate; the container still gets a remote to push to. Local-only scratch projects. |
false |
set | Host never touches the remote; the container fetches directly from upstream. Useful when the host has no path to the remote (firewall, corporate proxy) but the container does. |
false |
absent | No git plumbing at all; container workspace starts empty. |
When upstream_url is absent, security_class collapses — online and
gatekeeping describe the same act, since there is nothing to push
beyond the gate. Both values are accepted; they behave identically.
gatekeeping + gate.enabled: false is rejected at load time —
gatekeeping is the gate-enforced mode, so disabling the gate in that
mode is incoherent.
Gate sync semantics¶
terok project gate-sync <project> (and the TUI's g action, and the
auto-sync poller) updates the gate from upstream, but only ever applies
safe branch changes on its own: creating branches that appeared
upstream and fast-forwarding existing ones. Everything destructive — a
branch deleted upstream (the usual squash-merge cleanup), or a
force-pushed rewrite — is reported as a pending change and applied
only after you confirm it (interactive prompt in the CLI, modal in the
TUI, --destructive for scripts). Agent branches that exist only on
the gate are never touched, no matter how many syncs run.
Each pending change is labelled honestly: no gate-local commits means the gate copy is exactly what upstream last advertised (nothing can be lost), while would discard N gate-local commit(s) means agent work sits on that branch — confirm those only when you know the work is merged or obsolete.
Backups are saved from both sides of the gate: before any confirmed sync
change is applied, and — via the gate's post-receive hook — whenever an
agent force-pushes or deletes a branch through the gate. Either way the
old tip lands under refs/terok/backup/<branch>/…, so nothing an agent
overwrites is ever the last copy. Inspect and recover with:
terok project gate-backups myproj # list saved tips
terok project gate-backups myproj --restore <ref> # move a branch back to a backup
terok project gate-backups myproj --delete <ref> # drop one backup early
terok project gate-backups myproj --prune # expire old backups now
A --restore is itself reversible: the tip it replaces is backed up
first, and the branch move is compare-and-swap guarded, so a restore
that races an agent push fails loudly instead of clobbering the newer
tip. The same list/restore/delete actions are available in the TUI from
the project screen (B).
Backups expire automatically after 30 days (checked during sync), behind
which the gate's always-on reflog (gc.reflogExpire, 90 days) is the
unnamed last resort. Configure per project:
gate:
backups:
enabled: true # false: rely on the gate's reflog only
retention_days: 30 # 0: keep forever
A possible convention for teams that want one branch to always track upstream: treat the project's default branch as upstream-owned — agents branch off it but never commit to it directly, so its fast-forwards always apply cleanly and it never goes pending. terok does not enforce this; it falls out of the safe-sync rules naturally.
# Host blocks outbound SSH to github, but the container's network
# allowlist reaches github via its own rules.
project:
name: cp2k
security_class: online
git:
upstream_url: git@github.com:user/cp2k.git
gate:
enabled: false
Auto-deduction from host git config
If human_name and human_email are not set, terok deduces them from
the host's git config user.name and git config user.email.
From Zero to First Run¶
The quickest way to manage projects is through the TUI — run terok after
install. The steps below show the equivalent CLI workflow.
Prerequisites¶
- Podman installed and working
- OpenSSH client tools (ssh, ssh-keygen) for private Git over SSH
- tmux (optional, for the TUI under tmux and persistent container sessions)
Step 1: Create Project Directory¶
Step 2: Create project.yml¶
# ~/.config/terok/projects/myproj/project.yml
project:
name: myproj
security_class: gatekeeping # default — or "online" for direct push
image:
base_image: docker.io/library/ubuntu:24.04
user_snippet_file: user.dockerinclude # optional
git:
upstream_url: git@github.com:yourorg/yourrepo.git # optional; omit for local-only
default_branch: main
# authorship: human-agent # optional: author = human, committer = agent
See Gate and upstream combinations
for the four cells of the (gate.enabled, upstream_url) matrix —
scratch projects, firewalled hosts, and the defaults for each.
Step 3: (Optional) Image Snippet¶
Create ~/.config/terok/projects/myproj/user.dockerinclude:
This text is pasted near the end of your project image (L2) Dockerfile.
Step 4: Generate Dockerfiles¶
Step 5: Build Images¶
# Build only L2 project images (fast, reuses existing L0/L1 layers)
terok project build myproj
# Refresh just the agent-install layers (cache bust from the AGENT_CACHE_BUST point)
terok project build myproj --refresh-agents
# Rebuild from L0 (no cache) (includes base image pull and system packages)
terok project build myproj --full-rebuild
# Pick which agents get baked into L1 for this build (one-shot override)
terok project build myproj --agents claude,codex
terok project build myproj --agents all
# Optional: build a dev image from L0 as well
terok project build myproj --dev
Build modes:
- Default (build): Only rebuilds L2 project images, reuses existing L0/L1. Use for project config changes.
- --refresh-agents: Rebuilds L1+L2 and cache-busts the per-agent install layers, leaving the system-package layer intact. Use when an agent CLI has a new release.
- --full-rebuild: Rebuilds L0+L1+L2 with --no-cache --pull=always. Use when the base image or system packages need updating.
- --agents <list>|all: One-shot override of the agent selection for this build. Does not modify project.yml.
Choosing which agents to bake in¶
The L1 (agent) image can be built with a subset of the roster instead of "everything". The selection flows from (narrowest wins):
- Per-build CLI override —
terok project build --agents claude,codex(above). - Per-project default —
project.yml: - Global default —
~/.config/terok/config.yml:
Different selections produce different L1 image tags (terok-l1-cli:<base>-claude-codex, ...-gh-glab, ...) so multiple selections can coexist in the local image store. The OCI label ai.terok.agents on each L1 image records the exact selection for introspection; inside the container the same list is in /etc/terok/installed.env and the hilfe banner filters its output to match.
Transitive dependencies are expanded automatically — picking blablador or kisski also pulls in opencode.
Custom LLM endpoint providers¶
An LLM endpoint provider defines a runtime connection. An LLM endpoint provider
does not install an agent in the image. Store each provider file at
~/.config/terok/providers/<name>.yaml. If you set $XDG_CONFIG_HOME, Terok
uses $XDG_CONFIG_HOME/terok/providers/<name>.yaml instead. Terok uses <name>
as the provider name. Use only lowercase ASCII letters and digits for <name>.
The example.yaml file defines the example provider:
label: Example AI
upstream: https://api.example.com
auth:
api_key: {}
serves:
openai-chat: /v1
default_model: example-chat
models:
example-chat:
name: Example Chat
limit:
context: 120000
The empty api_key mapping uses the Authorization: Bearer <key> format. Set
header and prefix when the provider requires a different format. If you set
header but omit prefix, Terok uses no prefix for compatibility with legacy
files.
Omit models to let OpenCode and Pi request /models. OpenCode and Pi use only
the entries in a nonempty models map. When models is nonempty, OpenCode and
Pi do not request /models. Use a nonempty models map when the provider does
not implement /models.
The limit.context and limit.output fields accept positive token counts. Pi
uses each specified limit. Pi applies a default value to each omitted limit.
OpenCode uses limits only when you specify both fields. The OpenCode schema
requires both fields.
Authenticate the provider on the host. Then start a new task. In the task, select the provider with OpenCode or Pi:
terok auth example
terok task run myproj
# In the task container:
opencode --provider example
pi --provider example
After terok auth succeeds, Terok regenerates the vault routes. Terok also
checks the routes before each task launch. Do not run terok vault routes
manually.
Changes to a provider file without an install section do not require an image
rebuild. Install a compatible harness before you use the provider. The following
image configuration selects OpenCode and Pi:
Do not add example to image.agents. Rebuild L1 only when you add a harness
to the image.
When Terok creates a task container, Terok also creates the credential handles. A running task does not receive a provider that you authenticate later. Start a new task after you authenticate a provider or change its file. You do not need to restart the Terok TUI.
Terok also reads legacy files from ~/.config/terok/agent/providers/. Terok
supports full opencode: blocks in legacy files. A file in
~/.config/terok/providers/ overrides a legacy file with the same name. Run
terok config paths to show the primary user provider directory.
Step 6: Initialize SSH (for private repos)¶
Mints an ed25519 keypair into the vault credential database (no on-disk key files in the project directory). The public key is printed on success — register it as a deploy key on your Git host.
Repeating ssh-init reuses the scope's default key. To add another key, use
terok ssh add myproj; -c NAME (or --comment NAME) sets its comment.
Without a supplied comment, new keys use the next unused myproj-N name.
Interactive creation offers that name for editing before generating the key.
terok ssh list --scope myproj # Inspect key IDs and the default marker
terok ssh pub myproj # All assigned public keys, default first
terok ssh pub myproj --key-id 7 # One public key
terok ssh default myproj 7 # Offer this already-linked key first
The default belongs to the scope–key link: a shared key can be default in several projects independently. The first assigned key becomes default; adding or renaming keys does not change it. Removing the default promotes the oldest remaining assignment. Comments never determine priority.
In the SSH routing TUI, press p to show the selected public key for
copying, n to mint with an editable name, and f to make a linked key
the selected scope's default. The default link is marked with *.
Step 7: Create and Run a Task¶
Authenticate each provider the task will use before starting the task — see Authentication for commands and for why late auth does not reach already-running containers.
# Create a new task and attach into its shell (default on a TTY).
# Equivalent to: task run + waiting for ready + terok login — one command.
terok task run myproj
# --no-attach keeps the old behaviour: start the container and print the
# `terok login` instructions instead of exec'ing into it. Scripts piping
# terok's output get --no-attach automatically (non-TTY = non-interactive).
terok task run myproj --no-attach
# If the project's image hasn't been built yet, task run prompts on a TTY
# ("Build now? [Y/n]") and runs `project build` inline; scripts exit with
# a hint pointing at the command.
# List tasks
terok task list myproj
Additional Task Operations¶
# Rename a task
terok task rename myproj v9krt fix-auth-bug
# Follow up on a completed/failed headless task with a new prompt
terok task followup myproj v9krt -p "Now add tests for the fix"
# View formatted container logs
terok task logs myproj v9krt # Latest logs
terok task logs myproj v9krt -f # Follow live output
terok task logs myproj v9krt --tail 50 # Last 50 lines
terok task logs myproj v9krt --raw # Raw podman output
# Stop or restart a task
terok task stop myproj v9krt
terok task restart myproj v9krt # Resume as-is; warns if the image drifted
terok task restart myproj v9krt --recreate # Recreate to pick up a rebuilt image
# Delete a task
terok task delete myproj v9krt
# View archived (deleted) tasks and their logs
terok task archive list myproj
terok task archive logs myproj 20260305T143000Z
Step 8: Log into a Running Container¶
# Open a shell in a running task container (persistent tmux session)
terok login myproj v9krt
# Any unambiguous prefix works — terok resolves it against the live task list
terok login myproj v9k
terok login myproj v9
This opens a tmux session inside the container. The session persists across
disconnects — re-running terok login reattaches to the same session.
Interactive shells show hilfe --kurz on entry; run hilfe inside the
container for the fuller in-container help.
login and the other per-task verbs (task stop, task restart,
task delete, task logs, task followup, task rename) all accept an
unambiguous prefix in place of the full task ID.
From the TUI¶
Press i on any running task to open a login session. The TUI picks the best
method automatically:
| Environment | What happens |
|---|---|
| Inside tmux | Opens new tmux window (TUI stays visible) |
| Desktop (GNOME/KDE) | Opens new terminal window |
| Plain terminal | Suspends TUI, opens shell, resumes on exit |
Web (terok-web / textual serve) |
CLI login is disabled — there is no host terminal; an error notification points at toad mode. Run a task in toad mode for an in-browser session. |
Running the TUI under tmux (recommended)¶
This wraps the TUI in a managed tmux session with a blue status bar showing
keyboard shortcuts. Login sessions open as additional tmux windows — press
^b n/^b p to switch between TUI and container shells. Logging into a
container that already has a window open switches to that window instead of
opening a duplicate.
To launch the TUI in tmux by default without passing --tmux every time, set
it in your config:
--no-tmux overrides the config setting for a single launch.
When tmux mode is active, terok attaches to the shared terok session if
one is already running, so a second terok tui --tmux reconnects to your
existing TUI instead of failing on the duplicate session name. Resuming lands
you directly on the window running the TUI (even if you had killed and
relaunched it in a different window), and if none is running anymore — you quit
the TUI while task windows kept the session alive — it revives the TUI back in
its original spot as window 1. Pass --new-session to opt out and start a
separate, tmux-named session alongside it.
Inside a terok-managed tmux, a few extra conveniences apply:
- Quitting the TUI asks whether to return to your terminal (
qagain — the session and its tasks keep running in the background) or hop to the next tmux window (n). If the TUI window closes anyway, a brief status-bar hint in the window you land on explains how to get back. - Upgrades: if a newer terok is installed on disk while the TUI is
running (e.g.
pipx upgrade terok), the TUI offers to restart itself in place to pick up the new version. The offer appears as soon as you come back to the TUI — re-attaching the session or switching back to its window — or within ten minutes if you stay put.
tmux Quick Reference¶
| Context | Prefix | Status bar color | Common keys |
|---|---|---|---|
| Host tmux | ^b |
Blue | ^b n/p switch windows, ^b d detach |
| Container tmux | ^a |
Green | ^a n/p switch windows, ^a c new window |
The container's tmux prefix (^a) is different from the host's (^b) to avoid
conflicts. The container status bar shows host: ^b as a reminder.
Authentication¶
Before running tasks, authenticate each provider you plan to use. Credentials are stored host-wide in the vault and shared across every project and task by default.
# Host-wide auth — one login per provider, usable by every project
terok auth claude
terok auth gh
terok auth codex
# Project-scoped. OAuth uses and validates the project's L2 image.
# API-key endpoint auth uses no container and needs no image.agents entry.
# With the default credentials.scope: shared the token still lands in the
# host-wide bucket; credentials.scope: project stores it in the project's
# private vault bucket instead.
terok auth claude --project myproj
terok auth example --project myproj
# Interactive menu — pick one or more providers in sequence
terok auth
The interactive menu marks entries that already hold a stored credential
with ✓ authenticated (scoped to the project's vault bucket when
--project is given); the TUI's auth modal shows the same badge.
Re-authenticating a badged entry replaces the stored credential — the
single-provider form prints a heads-up first. When the vault can't be
read — not yet provisioned or currently sealed — both surfaces say so
instead of guessing.
Each provider offers the methods its vendor supports — OAuth / interactive
login (launches an auth container), the OAuth device-code variant (for
vendors that support it, e.g. Codex), and API key (paste, no container
needed). When a provider offers more than one, terok auth shows a chooser;
the TUI's auth flow presents the same choices.
On a headless host (no local browser), Codex's OAuth login can't open the usual browser callback. The device-code flow runs the same login headlessly — it shows a URL and a short code to enter on another device. Pick it from the chooser, or skip straight to it with the flag:
Container snapshot: auth must exist before task run¶
Agent env vars (and the phantom tokens that route each container's requests through the vault) are baked at container creation time. Two consequences follow from this:
- Missing auth is permanent for that container. If you start a
task without having authenticated provider X, the container has no
env var for X — and it never will, even if you
terok auth Xlater. The credential exists in the vault, but the running container has no phantom token pointing at it. To pick up the new credential, start a fresh task; existing ones stay blind. - Refreshing an existing auth works transparently. If provider X was authenticated when the task started, replacing the credential (new OAuth token, rotated API key) needs no restart — the container's phantom token resolves through the vault to whatever the current credential is at request time.
Run terok sickbay <project> <task> to see a per-task auth report.
Missing phantom env vars show up as warnings ("not set") and flag exactly
which providers were unauthenticated when the container was created.
Vault passphrase backend¶
The credentials DB itself is SQLCipher-encrypted; the passphrase
travels through a five-tier resolver chain (session-unlock file →
systemd-creds → OS keyring → credentials.passphrase_command helper →
interactive prompt). terok vault unlock writes to the session tier,
terok vault lock clears every stored copy, and setup picks the
persistent tier at install time.
To change the passphrase, run terok vault passphrase change
(or the [c]hange action on the TUI's Vault screen): it re-encrypts
the DB under the new key and rewrites every tier that stored the old
one, then walks you through saving the new recovery key. Re-encryption
needs the DB exclusively, and every running task's supervisor holds it
open — so with tasks running the flow offers to stop them, re-encrypt,
and restart them afterwards (their containers keep their state). A
supervisor orphaned by an earlier stop that still pins the DB is
detected and offered for cleanup; a process outside terok holding it
is named and never touched. To move the
same passphrase between backends, terok vault passphrase
to-keyring / seal are the first-class paths — see
credentials-encryption
in terok-sandbox for the full tier guide.
Pressing PANIC hard-locks the vault: it destroys every stored passphrase tier (session file and persistent tiers), so nothing can auto-unlock the vault afterwards. Recovery means re-supplying the escrowed recovery passphrase.
Headless Agent Runs (Unattended)¶
Run any supported agent headlessly in a container — no interactive session needed. Useful for CI/CD pipelines, batch tasks, or scripted workflows.
Use --agent with claude, codex, copilot, opencode, pi, or vibe.
Use --provider to select an LLM endpoint provider such as blablador,
kisski, or openrouter. OpenCode and Pi support compatible LLM endpoint
providers.
Basic Usage¶
# Run with a prompt (uses the default agent — claude unless configured otherwise)
terok task run myproj --mode headless --prompt "Fix the authentication bug in login.py"
# Override model and set a timeout
terok task run myproj --mode headless --prompt "Add unit tests for utils.py" --model opus --timeout 3600
# Detach immediately (don't stream output)
terok task run myproj --mode headless --prompt "Refactor the database layer" --no-follow
# Use a specific agent
terok task run myproj --mode headless --prompt "Fix the auth bug" --agent codex
terok task run myproj --mode headless --prompt "Add tests" --agent copilot
The command creates a new task, starts a container, runs the agent with the given prompt, and streams the output. When the agent finishes, the task is marked as completed and a diff summary is printed.
Default Agent¶
The agent is resolved in this order:
1. --agent flag (if given)
2. default_agent in project config (project.yml)
3. default_agent in global config (config.yml)
4. claude (ultimate fallback)
# Set per-project default in project.yml (top-level key)
default_agent: codex
# Set global default in config.yml
default_agent: claude
Agent Feature Matrix¶
| Feature | claude | codex | copilot | opencode | pi | vibe |
|---|---|---|---|---|---|---|
--model |
Yes | Yes | Yes | Yes | Yes | Yes (maps to --agent) |
| Session resume | Yes | No | No | Yes | Yes | Yes |
| Structured log output | Yes | No | No | No | No | No |
The selected harness determines the available capabilities for an LLM endpoint provider.
Per-Provider Config Values¶
Config keys like model and timeout live in the agent: section of
project.yml or config.yml and can be set per-provider using a dict
syntax. A flat value applies to all providers:
A dict maps each provider to its own value, with _default as fallback:
agent:
# Per-provider values
model:
claude: opus
codex: codex-mini
vibe: mistral-small
_default: fast
timeout: 1800 # flat values still work
Providers not listed in the dict (and without _default) use their own built-in
default. CLI flags (--model, --timeout) always override config values.
Features a provider doesn't support produce a warning but don't block the run.
Permission Mode (Unrestricted / Restricted)¶
By default, terok starts agents in unrestricted mode — all safety prompts are auto-approved so the agent can work fully autonomously. You can switch to restricted mode, which launches the agent with its vendor-default permission settings (the agent will ask for confirmation before dangerous operations like file writes or shell commands).
CLI flags¶
# Run restricted (agent uses vendor defaults — asks before dangerous ops)
terok task run myproj "Fix the bug" --restricted
# Explicitly unrestricted (default behavior)
terok task run myproj "Fix the bug" --unrestricted
The flags are mutually exclusive. When neither is given, the value comes from config (see below), defaulting to unrestricted.
Config¶
Like other config keys, unrestricted lives inside the agent: section
and follows the resolution stack:
global config → project config → CLI flag.
# In project.yml or global config
agent:
# Flat value — same for all providers
unrestricted: false # all agents start restricted
Per-provider dict syntax is supported:
Providers not listed in the dict (and without _default) default to
unrestricted (true).
What each mode does per agent¶
| Provider | Unrestricted mechanism | Restricted (vendor default) |
|---|---|---|
| claude | /etc/claude-code/managed-settings.json → "defaultMode": "bypassPermissions" |
Normal interactive prompts |
| codex | --yolo (injected by the shell wrapper) |
Sandboxed with approval prompts |
| copilot | COPILOT_ALLOW_ALL=true |
Tool confirmation prompts |
| vibe | VIBE_BYPASS_TOOL_PERMISSIONS=true |
Approval prompts |
| opencode / blablador | OPENCODE_PERMISSION='{"*":"allow"}' |
Default permission policy |
Checking the current mode¶
The TUI task detail panel also shows the permission mode.
Debug Mode¶
Each container's services (vault proxy, clearance hub, SSH signer, gate) run as
separate supervised child processes. In production every child hardens itself —
it clears the dumpable flag (PR_SET_DUMPABLE=0, so no ptrace, no core dump),
zeroes RLIMIT_CORE, and mlockalls its memory. That is exactly what you want
in normal operation, but it also makes the split's other payoff — a debuggable
per-service process you can attach to — unreachable.
--debug relaxes that floor for one task launch:
Only the dumpable clear is skipped, so a debugger can attach
(py-spy dump --pid <child>, gdb -p <child>); the core-file limit and
mlockall still apply. The choice is fail-closed: hardening is relaxed only
when this explicit flag is set — it is never inferred from -O / __debug__.
Debug mode is a CLI-only trigger — there is no TUI control to enable it
(asking for it presumes you'll attach a debugger, so the launch stays in the
terminal). A debug-mode task is marked read-only in the TUI task list with a
🪳 badge, and the choice is remembered across task restart.
Native Claude Agents and MCPs¶
Sub-agents, skills, and MCP servers are managed natively by Claude — terok
does not own a sub-agent abstraction of its own and does not inject any
--agents flag. Drop your definitions into the shared Claude config mount
(terok agents dir claude prints its path) and Claude discovers them on its
own, exactly as it would outside a container:
| What | Where |
|---|---|
| Global agents | ~/.claude/agents/ |
| Global MCPs | ~/.claude/settings.json (mcpServers section) |
| Project agents | <workspace>/.claude/agents/ |
| Project MCPs | <workspace>/.claude/settings.json |
Any extra flags you pass to the in-container claude command are forwarded
straight through, so per-invocation customization needs no terok support.
Run terok config paths to see the actual paths on your system.
Agent Instructions¶
terok provides layered agent instructions that describe the container environment to AI agents. Instructions are delivered automatically — no setup required.
How It Works¶
Every task container receives instructions explaining the workspace layout, available tools, sudo access, git workflow, and conventions. Two independent layers control what a task receives:
- YAML
instructionskey — controls the inheritance chain via config stack. Uses_inheritin list form to splice the bundled default at that position. Absent = bundled default. -
Standalone
instructions.mdfile in the project root — always appended at the end of whatever the YAML chain resolved. Purely additive. If empty or absent, nothing is appended. -
Claude: injected via
--append-system-prompt(system-level context) - Codex: loaded from
/home/dev/.terok/instructions.mdvia-c model_instructions_file=...in the wrapper - Other providers: prepended to the task prompt (headless
terok task run)
Scenarios¶
YAML instructions |
File | Result |
|---|---|---|
| absent | absent | bundled default |
| absent | has content | bundled default + file |
["_inherit"] |
has content | bundled default + file (same, explicit) |
["_inherit", "extra"] |
has content | bundled default + extra + file |
["custom only"] |
absent | custom only (no default) |
["custom only"] |
has content | custom only + file |
[] |
has content | file only |
"flat string" |
has content | flat string + file |
Customizing Instructions¶
Option 1: Standalone file (recommended for most users)¶
Create instructions.md in your project root with project-specific notes. These are appended to the bundled default automatically:
Option 2: YAML config¶
Set the instructions key in your project's agent: config:
# project.yml — flat string (replaces default)
agent:
instructions: |
You are in a Podman container. This project uses uv.
Run `make check` before committing.
Per-provider instructions:
agent:
instructions:
claude: |
Use Claude-specific conventions...
codex: |
Use Codex-specific conventions...
_default: |
Generic instructions for all providers.
Extend (rather than replace) parent instructions using _inherit:
# Project config — appends to global/default instructions
agent:
instructions:
- _inherit
- |
## Project-specific additions
This project uses uv for dependency management.
Run `make check` before committing.
To suppress defaults entirely, use an empty list:
CLI Flag¶
Override all config-stack instructions with a file:
TUI¶
The project details panel shows an Instruct: badge with three states:
- default (dim) — no custom instructions
- custom + inherited (green) — has custom content with defaults included
- custom only (cyan) — has custom content, defaults overridden
Available actions from the project details screen:
- Shift+I — edit project
instructions.mdin$EDITOR - t — toggle instructions inheritance (include/exclude bundled defaults)
- v — view fully resolved instructions as a task would receive them
Command palette (Ctrl+P) actions:
- Edit Global Instructions — edit the global
instructions.mdin$EDITOR - Show Default Instructions — view the bundled default instructions (read-only)
Theme¶
Pick a theme from the command palette (Ctrl+P → "Change theme"); the choice
is saved to tui.theme in your global config.yml and applied on the next
launch. The built-in ansi-dark theme renders with your terminal's own
palette and default background — console output (image builds, gate syncs)
then looks exactly as it would running the command directly in your terminal.
Debugging¶
Resolved instructions are always written to <tasks_root>/<task_id>/agent-config/instructions.md on the host for inspection.
Task Lifecycle Hooks¶
Hooks run user-configured shell commands on the host at key points during a task container's lifecycle. They are useful for port forwarding, notifications, logging, or custom setup/teardown.
Hook points¶
| Hook | When | Use case |
|---|---|---|
pre_start |
Before the container is created | Validate prerequisites, set up host resources |
post_start |
After the container is running | Start sidecars, register with service discovery |
post_ready |
After the application is ready (CLI ready marker / toad serving) | Port forwarding, open browser, notify |
post_stop |
After the container stops | Clean up port forwards, notify, archive logs |
Configuration¶
Hooks can be set globally (all projects) or per-project:
# ~/.config/terok/config.yml (global)
run:
hooks:
post_ready: ~/.config/terok/hooks/on-ready.sh
post_stop: ~/.config/terok/hooks/on-stop.sh
Environment variables¶
Hook commands receive task context via environment variables:
| Variable | Description | Example |
|---|---|---|
TEROK_HOOK |
Hook name | post_ready |
TEROK_PROJECT_NAME |
Project name | myproject |
TEROK_TASK_ID |
Task ID | v9krt |
TEROK_TASK_MODE |
Task mode | cli, toad, run |
TEROK_CONTAINER_NAME |
Podman container name | myproject-toad-v9krt |
TEROK_WEB_PORT |
Web port (toad only) | 18701 |
TEROK_TASK_DIR |
Host-side task directory | /home/user/.local/share/terok/sandbox-live/tasks/myproject/v9krt |
Hook tracking and sickbay¶
Hooks are tracked in task metadata (hooks_fired list). If a task stops
without its post_stop hook running (e.g. after a crash or host reboot),
terok sickbay detects the inconsistency:
terok sickbay # check all projects
terok sickbay myproject # check one project
terok sickbay myproject v9krt # check one task
terok sickbay --fix # auto-reconcile (run missed hooks)
terok sickbay --system # host-wide checks only; skip the per-container walk (fast)
Example: task lifecycle logging¶
See examples/hooks/task-notify.sh for a simple example that logs
lifecycle events with task context to stderr.
Image Management¶
Manage terok container images (L0/L1/L2 layers) with the image subcommand.
# List all terok images with sizes
terok image list
# List images for a specific project
terok image list myproj
# Remove orphaned and dangling terok images
terok image cleanup
# Preview what would be removed without actually deleting
terok image cleanup --dry-run
Project Management¶
Deleting a Project¶
Remove a project and all its associated data (tasks, containers, images):
# Delete with confirmation prompt
terok project delete myproj
# Skip confirmation
terok project delete myproj --force
Deriving a Project¶
Create a new project from an existing one (shared infrastructure, fresh agent config):
OpenCode Config Import¶
Import an OpenCode config file into the shared mount:
Container Timezone¶
Task containers follow the host's timezone by default — no configuration needed. Override per-project when you need a fixed zone (for example, to pin CI to UTC regardless of where the runner lives):
The value is any IANA zone name; it is resolved inside the container
against the image's tzdata. Omitting run.timezone (or setting it to
an empty value) falls back to host-follow — terok reads the host's
$TZ, then /etc/timezone, then the /etc/localtime symlink — and
leaves TZ unset if none of those resolve.
GPU Passthrough¶
GPU passthrough is a per-project opt-in feature (disabled by default). NVIDIA, AMD, and Intel GPUs are supported — individually or together.
Enable in project.yml¶
or select vendors — and single devices — explicitly:
run:
gpus: amd # one vendor, all of its devices
# gpus: "amd:1" # one device (quote — the selector contains a colon)
# gpus: "nvidia:0,nvidia:1" # two devices, token repetition
# gpus: [nvidia, amd, intel] # YAML list — all three into one container
all passes through every vendor whose host support terok detects and
fails only when none is found; naming a vendor explicitly fails loudly
at launch when that vendor's prerequisites are missing. A whole-vendor
token absorbs that vendor's indexed ones.
Device index semantics. On CDI hosts the index is handed to the
vendor's spec (--device amd.com/gpu=1) and means whatever the vendor's
tooling says it means — authoritative and enforced. Without CDI the
grant is best effort: terok orders a vendor's devices by PCI bus
address (stable across launches, and across reboots for unchanged
hardware, but not guaranteed to match rocm-smi/nvidia-smi
numbering), mounts only the selected device nodes, and prints a warning
recommending CDI. Inside the container a selected card enumerates as
device 0. Switching a host to CDI can renumber devices.
Per-vendor requirements and flags¶
For each vendor terok prefers a CDI
spec when one is present on the host (/etc/cdi, /var/run/cdi, or
podman's configured cdi_spec_dirs) and otherwise falls back to the
vendor's documented raw-device recipe. CDI needs podman ≥ 4.1; the raw
recipes also work on older podman releases.
NVIDIA — best served by the NVIDIA Container Toolkit
with a generated CDI spec (nvidia-ctk cdi generate). Three tiers,
picked automatically:
- CDI:
--device nvidia.com/gpu=all(or one=Nper selected device) - Pre-CDI toolkit installs (podman < 4.1): the toolkit's legacy OCI
hook triggers on the env vars and injects devices and driver
userland itself — terok emits only the env vars (device selection
included, via
NVIDIA_VISIBLE_DEVICES) - Driver without toolkit: the
/dev/nvidia*device nodes are passed — all of them, or the shared nodes plus the selected GPUs' nodes (minors resolved from/proc/driver/nvidia/gpus); the image must then carry a driver userland (libcuda) matching the host kernel module
All tiers set NVIDIA_VISIBLE_DEVICES (all or the selected indices)
and NVIDIA_DRIVER_CAPABILITIES=all.
AMD — CDI via the AMD Container Toolkit
(amd-ctk cdi generate, kind amd.com/gpu) when present; otherwise the
ROCm-documented
device pair (requires the amdgpu kernel driver):
--device /dev/kfdplus the AMD render nodes (or--device amd.com/gpu=allvia CDI)--group-add keep-groups
Intel — CDI (kind intel.com/gpu) when present; otherwise the
Intel render nodes — the oneAPI / Level Zero / OpenCL stack needs
nothing else (requires the i915/xe kernel driver):
- the Intel
/dev/dri/renderD*nodes (or--device intel.com/gpu=allvia CDI) --group-add keep-groups
Raw DRM grants are vendor-scoped: only the granted vendor's render
nodes are mounted, never the whole /dev/dri — on a mixed host the
other vendors' GPUs (and non-GPU DRM devices such as BMC display
adapters) stay invisible to the container. Each granted node's
/dev/dri/by-path symlink is bound read-only alongside it, keeping
Intel NEO's PCI-ordered enumeration path working and multi-GPU device
identity legible inside the container.
--group-add keep-groups keeps the invoking user's host groups (the
render group gating AMD/Intel nodes) inside the rootless container.
It is honoured by crun only — if your podman defaults to runc,
install crun and set run.runtime: crun. The invoking user must be in
the host's render group.
Base image¶
Pick a base image matching the vendor's userland in project.yml, e.g.:
image:
base_image: nvcr.io/nvidia/nvhpc:25.9-devel-cuda13.0-ubuntu24.04 # NVIDIA
# base_image: rocm/dev-ubuntu-24.04:latest # AMD ROCm/HIP
# base_image: intel/oneapi-basekit:latest # Intel oneAPI/SYCL
For raw-tier NVIDIA hosts (no toolkit, no CDI) the image must carry a matching driver userland — a proven recipe lives in Custom Base Images.
Selecting GPUs inside the container¶
Prefer selecting at the terok level (gpus: "amd:1") — the container
then never sees the other devices. For finer runtime control inside a
container that was granted several devices, the vendors' own selectors
still work: CUDA_VISIBLE_DEVICES (NVIDIA), ROCR_VISIBLE_DEVICES /
HIP_VISIBLE_DEVICES (AMD), ONEAPI_DEVICE_SELECTOR /
ZE_AFFINITY_MASK (Intel) — indices are container-local.
Running Containers Inside Your Container¶
Projects that run podman or docker inside their terok container (for example, projects developing container tooling, or those needing fuse-overlayfs) need the outer container launched with two extra flags. Declare this once in project.yml:
When set, terok appends to the outer podman run:
--security-opt label=nested— the SELinux type that confines the outer container but permits nested container operations (devpts mount, rootless overlay setup). This is notlabel=disable— SELinux stays enforced.--device /dev/fuse— required by rootless podman'sfuse-overlayfsstorage driver.
Verify inside the container:
Requirements¶
- Podman ≥ v4.5.0 on the host (introduced
label=nested, April 2023 — every current distro ships 4.5+). - A base image with podman preinstalled; the bundled
online-podman/gatekeeping-podmanpresets point atquay.io/podman/stable:latest(Fedora-based, rootless-ready). - On SELinux-enforcing hosts:
container-selinuxpackage (usually already installed on Fedora/RHEL).
If the image doesn't have podman preinstalled, nested_containers: true still sets the capabilities — your project's user snippet can install the runtime:
image:
base_image: fedora:44
user_snippet_inline: RUN dnf install -y podman fuse-overlayfs
run:
nested_containers: true
Profiling Inside the Container (perf)¶
Sampling with perf needs the perf_event_open syscall, which the
default container seccomp profile denies unless the container holds the
perfmon capability. Declare it once in project.yml:
terok grants perfmon through a typed, allowlisted channel (never a
freeform flag), which flips the seccomp rule to allow. Install the
tool inside the task (sudo dnf install perf) or bake it into a
custom image.
Host prerequisite. Rootless containers can never hold CAP_PERFMON
in the initial user namespace, so the kernel's
kernel.perf_event_paranoid sysctl still applies — it is a host-global
switch no container flag can substitute for:
2 is the mainline kernel default; hardened kernels (Ubuntu) ship 4,
which disables unprivileged perf entirely — task launch warns when it
detects that. The resulting scope is sampling the task's own
processes, user-space call stacks only (perf record ./bench, flame
graphs via DWARF or frame pointers). System-wide profiling (perf -a)
and kernel-side samples are structurally unavailable to rootless tasks.
Custom Podman Flags (run.podman_args)¶
An expert escape hatch for launch flags terok has no dedicated knob
for — extra env vars, --add-host entries, port publishing, shm
sizing, ulimits:
run:
podman_args:
- "-e"
- "HTTPS_PROXY=http://host.containers.internal:8118"
- "--add-host"
- "my.model.example:10.0.0.5"
- "--shm-size=8g"
The list is appended verbatim to podman run — you own the pieces.
Two guard rails, enforced when project.yml is parsed and again at
launch:
- Sandbox-managed flags are rejected (
--network,--name,--cap-add,--userns,--annotation, …, and volume mounts that would shadow terok's/run/terok/sockets) — they would silently fight the launch assembly terok performs. Capability grants go through curated knobs likerun.perfinstead. - Isolation-weakening flags are rejected (
--privileged,--security-opt) — those invalidate the shield/supervisor assumptions and will only ever arrive as vetted, typed features.
Tips¶
- Show resolved paths:
terok config paths - Where credentials live:
~/.local/share/terok/vault(or/var/lib/terok/vaultif root; override withcredentials.dirin config.yml orTEROK_VAULT_DIR) - Shared directories: See shared-dirs.md
- Security modes: See git-gate-and-security-modes.md
- Copying text from the terminal: TUI and tmux can intercept mouse events, preventing normal text selection from reaching the clipboard. Hold Shift while selecting, then Shift+Ctrl+C to copy.
FAQ¶
Where are templates and scripts stored?¶
Loaded from Python package resources bundled with the wheel (under terok/resources/). The application never reads from /usr/share.