File: README.md
Terax is a lightweight open-source terminal-first AI-native development environment (ADE) built on Tauri 2 + Rust and React 19. A native PTY backend with a WebGL renderer, an agentic AI side-panel that runs against your own keys or fully local models, plus a code editor, file explorer, source control with a git graph, and a web preview pane built in. About 7-8 MB on disk. No telemetry. No account.
Screenshots
| Web preview of local dev servers | Agentic AI workflow with edit diffs in the code editor |
|---|---|
| Custom themes, presets, and background images | Source control panel with git graph in history |
| Block-based WebGL terminal with editor-like input panel |
Features
Terminal
- xterm.js with WebGL renderer, multi-tab with background streaming
- GPU-accelerated block-based terminal with editor-like command input
- Native PTY backend via
portable-pty(zsh, bash, pwsh, fish, cmd) - Split panels (horizontal and vertical)
- Inline search, link detection, true-color
- Drag files from the explorer or desktop into a terminal as shell-safe quoted paths
- Per-tab workspace environments on Windows (Local, or any installed WSL distro)
- Spaces restore tabs, working directories, and split layouts across launches
Code editor
- CodeMirror 6 (supports all popular languages - TS/JS, Rust, Python, Go, C/C++, Java, HTML/CSS, JSON, Markdown, etc.)
- Inline AI autocomplete with local model support
- AI edit diffs, accept or reject hunk by hunk
- Opt-in language server support with diagnostics, navigation, completion, formatting, and custom servers
- Rendered Markdown plus image, video, audio, and PDF viewing
- Vim mode
- Built-in editor themes including Kanagawa, Catppuccin, RosΓ© Pine, Everforest, Dracula, Solarized, Nord, Tokyo Night, GitHub, and Xcode
Source control
- Stage / unstage hunks, commit (Cmd+Enter / Ctrl+Enter), push with upstream awareness
- Branch display including detached HEAD state
- Git history pane with a real commit graph (lane rendering for merges and branches)
- Commit search and filter, click through to the remote commit page
File explorer
- Catppuccin icon theme
- Fuzzy search, keyboard navigation, inline rename, context actions
- Live updates when files change on disk
- Attach files and selections directly to the AI side-panel
Web preview
- Auto-detects local dev servers and opens them in a preview tab
- External URL preview via a native child webview
Themes and customization
- Custom themes built in-app, switch between bundled presets and your own
- Create your own themes, share them or import from the community
- Background images with adjustable opacity and blur
- Editor theme is independent from the app theme
AI
- BYOK providers: OpenAI, Anthropic, Google (Gemini), Groq, xAI (Grok), Cerebras, OpenRouter, DeepSeek, Mistral, plus any OpenAI-compatible endpoint
- Local / offline: LM Studio, MLX, Ollama
- Agentic workflow: plans, sub-agents, project memory via
TERAX.md, file read / write / edit / multi-edit / grep / glob, bash with approval gating, background processes - Coding-agent orchestration: spawn Claude Code in a terminal, inspect its output, and send follow-up work through approval-gated tools
- Composer: prompt snippets via
#handle, files via@path, voice input, attach-to-agent from explorer or selection - Custom agents with their own system prompt and tool subset
- Plan mode for multi-step work, generates and confirms before doing
Install
Latest installers are on the Releases page. Terax auto-updates from there.
Windows notes
- Default shell detection:
pwsh.exe(PowerShell 7+) ->powershell.exe(Windows PowerShell 5.1) ->cmd.exe. - WSL is a first-class workspace environment, not a wrapped subprocess.
Linux notes
- Arch / AUR:
yay -S terax-bin(orparu, etc.). Tracks the latest release. - NixOS / Nix: use the official flake -
nix profile install github:crynta/terax-ai(non-NixOS), or import the flake and addinputs.terax.packages.${pkgs.system}.teraxtoenvironment.systemPackages(NixOS). ThenixosModules.teraxoutput is also available for a simpler setup. - AppImage: needs FUSE. Without it:
./Terax_*.AppImage --appimage-extract-and-run. On Wayland with rendering glitches, tryWEBKIT_DISABLE_DMABUF_RENDERER=1. Otherwise the.deb/.rpmpackages link against the system GTK stack and tend to be smoother.
Configure AI
- Open Settings -> AI.
- Pick a provider and paste your API key. For local inference, point Terax at your LM Studio / MLX / Ollama endpoint.
- Keys are written to the OS keychain via
keyring. They never touch disk or localStorage.
Build from source
Prerequisites
- Rust (stable), https://rustup.rs
- Node 20+ and pnpm
- Tauri prerequisites for your platform, https://tauri.app/start/prerequisites/
Run
pnpm install
pnpm tauri dev # development
pnpm tauri build # production bundleChecks
pnpm lint
pnpm check-types
pnpm test
cd src-tauri && cargo clippy --all-targets --locked -- -D warnings # Rust lint (matches CI)
cd src-tauri && cargo nextest run --locked # or: cargo test --lockedTech stack
Tauri 2, Rust, portable-pty, React 19, TypeScript, Vite, xterm.js, CodeMirror 6, Vercel AI SDK v6, Tailwind v4, shadcn/ui, Zustand.
Contributing
Issues and PRs are welcome! Feel free to open issues, suggest features, or submit pull requests. See CONTRIBUTING.md and the architecture docs for more details.
Code signing
Windows builds are signed with a free code signing certificate provided by SignPath.io, certificate by the SignPath Foundation.
License
Terax is licensed under the Apache-2.0 License. For more information on our dependencies, see Apache License 2.0.
File: docs/architecture/ai-subsystem.md
AI subsystem
This guide elaborates on TERAX.md. If anything here conflicts with TERAX.md, TERAX.md wins.
Overview
The AI subsystem is BYOK (bring your own key). It supports cloud providers via @ai-sdk/* and local / offline providers via OpenAI-compatible endpoints. The agent layer is built on Vercel AI SDK v6 chat semantics: streamText, tool definitions, and stopWhen step limits.
Main entry point: runAgentStream in src/modules/ai/lib/agent.ts.
Providers
Cloud providers are defined in src/modules/ai/config.ts:
- OpenAI, Anthropic, Google, xAI, Cerebras, Groq, DeepSeek, Mistral, OpenRouter
openai-compatiblefor any custom base URL- Local: LM Studio, MLX, Ollama
buildLanguageModel in src/modules/ai/lib/agent.ts:76 branches on provider to construct the correct AI SDK provider instance. Local providers use createOpenAICompatible with a localProxyFetch that allows private-network access, while cloud providers use their dedicated SDK constructors.
Model metadata (context limits, costs, reasoning behavior) lives in the model registry in config.ts. resolveModel maps a model id to its provider and defaults.
Adding a new provider
- Add a
ProviderInfoentry toPROVIDERSinsrc/modules/ai/config.ts. - Add model ids and metadata to the model registry in the same file.
- Add a branch in
buildLanguageModel(src/modules/ai/lib/agent.ts:99) that constructs the provider instance. For OpenAI-compatible APIs you can often reusecreateOpenAICompatible. - If the provider requires an API key, update
providerNeedsKeyinconfig.tsand the keyring service mapping. - If it needs a dedicated
@ai-sdk/*package, add it topackage.jsonand justify the bundle cost (seeCONTRIBUTING.md). - New built-ins must justify unique value beyond
openai-compatibleand OpenRouter;CONTRIBUTING.mdcalls this out explicitly.
Keys are never persisted outside the OS keychain / Linux secrets file.
Agent run loop
runAgentStream (agent.ts:391):
- Resolves the model via
buildConfiguredLanguageModel. - Builds a stable system prompt from
selectSystemPrompt(modelId)plus optional persona, custom instructions, andTERAX.mdproject memory. - Converts UI messages to model messages, prunes reasoning content if the model does not keep it, and compacts old messages if the context limit is exceeded.
- Streams via
streamTextwith the tool set frombuildTools(ctx)andstopWhen: stepCountIs(MAX_AGENT_STEPS). - Emits step labels, usage deltas, and finish metadata.
The tool set is assembled in src/modules/ai/tools/tools.ts from fs, edit, search, shell, subagent, terminal, todo, and managedAgent builders.
Sub-agents
src/modules/ai/agents/registry.ts defines read-only sub-agents: explore, code-review, security, and general. Each has a whitelist of tools and its own system prompt. run_subagent cannot recurse (the subagent tool set excludes run_subagent itself).
Sessions
Conversations are organized into sessions. Persistence lives in terax-ai-sessions.json via tauri-plugin-store (src/modules/ai/lib/sessions.ts):
sessionskey: list of session metadataactiveIdkey: active session idmessages:keys: per-session messages, loaded lazily
AgentRunBridge mirrors active-session messages to disk on every change and auto-derives titles from the first user message.
Composer
AiComposerProvider (src/modules/ai/lib/composer.tsx) is a React context that holds shared input state (text, attachments, voice) for the docked input bar and any other surface. Attachments can be images, text files, or selection chips from the terminal or editor. Selections are wrapped as <selection source="terminal|editor">β¦</selection> blocks at submit time and are not pasted into the textarea.
The composer derives isBusy from agentMeta.status so it can mount safely before sessions hydrate.
Tools and approval
Tool definitions live under src/modules/ai/tools/:
- Read-only tools (
read_file,list_directory,grep,glob) auto-execute after passing the security deny-list. - Mutating tools (
write_file,edit,multi_edit,create_directory,bash_run,bash_background) setneedsApproval: true. The AI SDK pauses and the UI renders an approval card. edit/multi_editenforce a read-before-edit invariant: the model must have read the file earlier in the session.- In plan mode, mutating tools queue edits for batch review instead of applying them immediately.
Auto-send after approval uses lastAssistantMessageIsCompleteWithApprovalResponses.
Edit diffs
AI-proposed file edits open in an ai-diff tab. The user accepts or rejects per hunk. Only after acceptance does the write_file or edit tool actually run. This keeps the approval UI decoupled from the tool execution.
Live context bridge
App.tsx calls setLive({ getCwd, getTerminalContext, β¦ }) so tools can read the currently active terminal's cwd and the last 300 lines of buffer. It is lazy by design - tools call for it only when needed rather than pre-snapshotting every turn.
Invariants
- Keep the Vercel AI SDK v6 chat shape (
streamText, tools, step limits); the rest of the UI depends on it. - Keys only via
secrets_*commands; never disk, settings store, orlocalStorage. - New providers must justify their bundle cost and unique value.
- Mutating tools require approval; read-only tools still pass the deny-list.
See also
TERAX.md- the architecture source of truthdocs/README.md- index of contributor guides- Two-process model - IPC boundary and command catalog
- Security model - the boundaries every tool must respect
File: docs/architecture/pty-shell-integration.md
PTY shell integration
This guide elaborates on TERAX.md. If anything here conflicts with TERAX.md, TERAX.md wins.
Session model
A terminal tab maps to one PTY session. Sessions live in PtyState (src-tauri/src/modules/pty/mod.rs:20):
pub struct PtyState {
sessions: RwLock<HashMap>>,
next_id: AtomicU32,
}IDs start at 1 and monotonically increase; they are never reused so the frontend can treat 0 as unset.
pty_open (mod.rs:44) spawns a session on a blocking thread, inserts it into the map, and returns the id. Output streams through a Channel<Response>; exit codes stream through a separate Channel. pty_write (mod.rs:100) accepts raw bytes with an x-pty-id header to avoid JSON serialization on every keystroke.
Reader / flusher / waiter threads
session::spawn (session.rs:102) starts three threads per session:
- Reader - reads bytes from the PTY master, runs the DA filter and agent detector, and pushes filtered bytes into a pending buffer.
- Flusher - coalesces output and sends it to the frontend over the data channel.
- Waiter - waits for the child process to exit, flushes the tail, and emits the exit code.
The pending buffer is capped at 4 MiB; on overflow it is discarded and replaced with an SGR-reset notice so xterm state is not corrupted by a sliced CSI sequence.
Shell bootstrapping
shell_init::build_command (shell_init.rs:53) builds the CommandBuilder used to spawn the shell. The path and arguments depend on the platform and the selected workspace environment (Local or a WSL distro).
Unix
Integration scripts live in src-tauri/src/modules/pty/scripts/:
zshenv.zsh,zprofile.zsh,zlogin.zsh,zshrc.zshfor zshbashrc.bashfor bashinit.fishfor fish, installed to~/.config/fish/conf.d/terax.fish
Zsh is launched with ZDOTDIR pointing at a temp directory that sources our scripts and then the user's real configs. Bash uses --rcfile with a wrapper that sources the user's ~/.bashrc after Terax's. Fish uses conf.d so no user file is replaced.
All integrated shells emit OSC 7 (cwd) and OSC 133 A/B/C/D (prompt boundaries and exit code) so Terax can track cwd and detect command boundaries without parsing the user's prompt.
Windows
On Windows the shell priority is:
pwsh.exe(PowerShell 7+)powershell.exe(Windows PowerShell 5.1)cmd.exe(no integration)
PowerShell loads profile.ps1 via:
pwsh -NoLogo -NoExit -ExecutionPolicy Bypass -FileThe profile wraps the user's existing prompt function to emit OSC 7 + OSC 133 A/B/D after $PROFILE runs. The cwd is normalized to backslashes before being passed to ConPTY because CreateProcessW misbehaves with forward slashes.
Fish 4.0+
Fish 4.0 writes its own OSC 133 prompt markers. To avoid doubling, Terax sets fish_features=no-mark-prompt and re-asserts its own prompt via -C after config.fish runs.
Concurrency and process lifetime on Windows
`CONPTY_LIFECYCLE_LOCK`
openpty + spawn_command and the corresponding close are serialized by a static mutex in session.rs:71. Concurrent ConPTY lifecycle calls corrupt the new console so its shell never pumps output.
Job Object
Each ConPTY child is assigned to a Windows Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (job.rs:34). When the Job HANDLE drops - clean shutdown, panic, or even a SIGKILL'd Terax process - the kernel kills every descendant of the shell. Without this, TerminateProcess only kills the immediate child and npm run dev started inside pwsh would be orphaned.
On macOS and Linux, Drop for Session calls killer.kill(). Dev Ctrl-C of cargo run can still leave orphans because destructors may not run; that is acceptable for development only.
Input and escape-sequence handling
DA filter
PowerShell / PSReadLine sends a cursor-position query (ESC[6n) at startup and blocks until it gets an answer. The DaFilter (da_filter.rs) intercepts that query and replies on the PTY input so the shell does not hang.
Agent detection
The reader thread runs an AgentDetector (agent_detect.rs) over the byte stream. It is armed by OSC 133;C;<cmd> or by a self-armed OSC 777 marker and emits terax:agent-signal transitions (started, working, attention, finished, exited). Detection is driven only by OSC sequences, never by raw output, so a repainting TUI never flaps.
Enter key
Terminal input sends \r (CR), not \n (LF). PowerShell on Windows requires CR.
Invariants
- Do not remove
CONPTY_LIFECYCLE_LOCKwithout verifying first-tab stability under fast tab spam. - Do not disable the Job Object without a replacement orphan guard on Windows.
- Keep platform-specific shell logic in the matching
#[cfg(unix)]or#[cfg(windows)]arm ofshell_init.rs. - cwd passed to ConPTY must use backslashes; OSC 7 cwd arriving at the frontend is forward-slash canonical.
See also
TERAX.md- the architecture source of truthdocs/README.md- index of contributor guides- Two-process model - IPC boundary and command catalog
- Terminal renderer pool - slot pooling and the DormantRing
File: docs/architecture/security-model.md
Security model
This guide elaborates on TERAX.md. If anything here conflicts with TERAX.md, TERAX.md wins.
Terax runs shells, reads and writes files, and sends data to AI providers. The security model is defense-in-depth: no single guard is enough, so every boundary validates input before acting on it.
Boundaries
The main trust boundaries are:
- IPC boundary - commands registered in
src-tauri/src/lib.rs, gated bysrc-tauri/capabilities/default.json. - File-system boundary - AI tools go through
src/modules/ai/lib/security.ts; PTY spawn goes through the workspace authorization registry. - Network boundary - AI HTTP proxy in
src-tauri/src/modules/net.rswith SSRF and DNS-rebinding defenses. - Secret-storage boundary - keys live in the OS keychain, never on disk or in
localStorage. - Terminal escape-sequence boundary - OSC sequences are parsed and acted on, but never blindly trusted to mutate state.
Secret-path deny-list
src/modules/ai/lib/security.ts refuses reads and writes of obvious secret paths. This applies on both read and write and must never be bypassed.
Blocked categories include:
- Files:
.env*,*.pem,*.key,*.p12,id_rsa*,known_hosts,credentials,service-account*.json, and similar. - Directories:
~/.ssh,~/.gnupg,~/.aws,~/.kube,~/.config/gh,~/.git, system dirs (/etc,/proc,/sys), and Windows credential stores. - System write prefixes:
/etc/,/var/db/,/usr/bin/,/windows/,/program files/, etc.
The comparison surface normalizes paths: backslashes to forward slashes, strips Windows drive letters, strips NTFS alternate data streams, strips trailing dots/spaces, lowercases, and collapses duplicate slashes. Protected directories are matched as exact path or descendant, not raw substring.
checkReadableCanonical and checkWritableCanonical also canonicalize the path and re-check the resolved form so a symlink at an innocent path pointing into ~/.ssh is caught.
Workspace authorization registry
WorkspaceRegistry (src-tauri/src/modules/workspace.rs:20) tracks directories that PTY spawn, git commands, and AI tools are allowed to operate in.
workspace_authorizeadds a directory.authorize_spawn_cwdrejects a spawn cwd outside an authorized root.authorize_user_spawn_cwdregisters the user's chosen cwd as a new root instead of rejecting it.- The registry is bootstrapped with the launch directory and the user's home directory (
workspace.rs:135).
This is the allow side of the file-system boundary. Any new feature that spawns a shell or mutates files outside the current workspace must interact with this registry.
AI tool approval flow
In src/modules/ai/tools/tools.ts:
- Read-only tools (
read_file,list_directory,grep,glob) auto-execute after passing the deny-list. - Mutating tools (
write_file,edit,multi_edit,create_directory,run_command,shell_session_run,shell_bg_spawn) setneedsApproval: true. The AI SDK pauses and surfaces atool-approval-requestpart rendered as a confirmation card. edit/multi_editenforce a read-before-edit invariant: the model must have read the file earlier in the session.
Auto-send after approval uses lastAssistantMessageIsCompleteWithApprovalResponses.
SSRF and DNS rebinding defense
src-tauri/src/modules/net.rs proxies AI provider requests and local-model pings. Before connecting:
- Resolve the hostname once (
resolve_and_classify). - Classify every resolved IP as public, private, loopback, or blocked metadata.
- Block cloud metadata endpoints (
169.254.169.254,metadata.google.internal, AWS IPv6 metadata, etc.). - Pin reqwest to the resolved IPs so a second DNS lookup cannot return a different address (DNS rebinding).
Local LLM endpoints are explicitly allowed because the user opted in by pointing Terax at them, but they are still classified and logged.
Secret storage
API keys are stored via secrets_* commands (src-tauri/src/modules/secrets.rs):
- macOS: Keychain via
keyring - Windows: Credential Manager via
keyring - Linux: a JSON file in the app's local data dir with mode
0600(atomic write to.tmpthen rename)
Service constant: terax-ai. Keys never touch disk outside the keychain/Linux secrets file, never go in localStorage, and never appear in logs.
OSC trust gating
The terminal parses OSC sequences from the PTY byte stream:
- OSC 7 updates the tab cwd.
- OSC 133 A/B/C/D marks prompt/command boundaries.
- OSC 777 is used by the agent detector to signal coding-agent state transitions.
The agent detector (src-tauri/src/modules/pty/agent_detect.rs) is armed by OSC 133;C;<cmd> or by a self-armed marker and emits terax:agent-signal events. It is driven only by OSC sequences, never by raw output, so a repainting TUI never flaps.
Invariants
- The deny-list in
security.tsapplies on both read and write. Never bypass it. - New file-system-touching commands must respect the workspace authorization registry.
- New network-facing commands must go through the
net.rsproxy or reimplement the same classification and DNS pinning. - New plugin APIs must be added to
src-tauri/capabilities/default.json. - Keys, tokens, and credentials stay in the keychain / Linux secrets file.
See also
TERAX.md- the architecture source of truthdocs/README.md- index of contributor guides- Two-process model - IPC boundary and command catalog
- AI subsystem - tools, approval flow, and provider handling
File: docs/architecture/terminal-renderer-pool.md
Terminal renderer pool
This guide elaborates on TERAX.md. If anything here conflicts with TERAX.md, TERAX.md wins.
Why a pool exists
Terminal tabs are kept mounted and hidden on switch so PTYs and dev servers keep streaming in the background. Creating an unbounded number of live xterm + WebGL renderer instances would blow the memory budget, so Terax pools renderer slots.
The pool lives in src/modules/terminal/lib/rendererPool.ts.
Slot lifecycle
POOL_MAX_SIZEis 5 (rendererPool.ts:22). Each slot owns one xtermTerminal,FitAddon,SearchAddon,SerializeAddon, and optionally aWebglAddon.- A slot is created on demand and assigned to a leaf on bind.
releaseSlotdetaches a slot from a leaf. If the leaf is idle, the slot is parked withdisplay:noneso xterm stops rendering but keeps parsing PTY bytes.- After a grace period, idle slots may be reaped to keep the pool size down.
Parking vs releasing
When a leaf becomes hidden:
parkLeafSlotsets the host todisplay:none. Rendering pauses but the live buffer keeps receiving bytes.- If the leaf is busy (foreground command, agent signal, alt-screen TUI, or block-shell running mode), it keeps the slot parked indefinitely.
- If the leaf is idle,
releaseSlotis called afterHIDDEN_RELEASE_DELAY_MS. The slot'scurrentLeafIdis cleared andretainedLeafIdis set so the buffer stays live.
When the leaf becomes visible again, acquireSlot looks for:
- A slot already bound to this leaf.
- A retained slot for this leaf (
retainedLeafId === leafId) - fast path, no snapshot replay. - A clean idle slot.
- If the pool is at max size, the lowest-scoring slot is evicted. Eviction serializes the retained buffer to a snapshot via
SerializeAddonbefore stealing the slot.
The DormantRing
src/modules/terminal/lib/dormantRing.ts buffers PTY bytes for leaves that have no slot at all (stolen or never bound). It is capped at 1 MiB and drops oldest blocks on overflow. On drain it resumes from the next line boundary rather than resetting the terminal, so a mid-line escape sequence is not replayed from the middle.
The never-serialize-mid-command invariant
This is the most important rule in the pool. A leaf that is in the middle of a command must never be serialized. Replaying incremental TUI repaints over a stale snapshot is what used to wipe Claude Code.
The code enforces this by checking isLeafBusy before eviction and by keeping slots parked (not released) while commandRunning, isAgentActivePty, or alt-screen is true.
Fast path and snapshot replay
If a retained slot exists for a leaf, bindSlot skips term.clear() / term.reset() and simply drains the DormantRing into the live buffer. This avoids re-rendering a large snapshot.
If only a snapshot exists, bindSlot clears the terminal, resizes, writes the snapshot, then drains the ring. For alt-screen TUIs, the snapshot is skipped and a SIGWINCH kick is sent so the TUI repaints from scratch.
WebGL lifecycle
WebGL addons are created when a slot becomes visible and reaped after a grace period when parked. The addon recovers from context loss on sleep/wake or GPU reset.
Invariants
- Never allow the pool to grow without bound; max is
POOL_MAX_SIZE. - Never serialize or evict a leaf that is mid-command or in alt-screen.
- A hidden busy leaf keeps its live grid parked with
display:none. - An idle hidden leaf releases its slot but the buffer continues parsing bytes.
- The DormantRing only buffers bytes for leaves without any slot.
See also
TERAX.md- the architecture source of truthdocs/README.md- index of contributor guides- PTY shell integration - sessions, OSC sequences, and ConPTY
File: docs/architecture/two-process-model.md
Two-process model and IPC command reference
This guide elaborates on TERAX.md. If anything here conflicts with TERAX.md, TERAX.md wins.
The split
Terax is two processes: the Rust backend (src-tauri/) and the webview frontend (src/).
- Rust owns all OS access: PTY, file system, git, shell spawn, network, secrets, workspace authorization.
- The webview never touches the FS, processes, or shells directly. Every host operation goes through an
invoke()call to a command registered insrc-tauri/src/lib.rs.
This boundary is the root of the security model. Untrusted input (terminal escape sequences, file content, AI tool results) is parsed and validated in Rust or in carefully scoped frontend code, never executed by the renderer.
Adding a new IPC command
- Write the
#[tauri::command]async function in the appropriatesrc-tauri/src/modules/<area>/module. - Register it in
src-tauri/src/lib.rsinside thetauri::generate_handler![...]block (src-tauri/src/lib.rs:191). - If the command uses a Tauri plugin API (window, clipboard, dialog, etc.), add the plugin permission to
src-tauri/capabilities/default.json. - Add a typed frontend wrapper in the matching
src/modules/<area>/lib/directory and call it through Tauri'sinvoke()API. - If the command touches the file system, network, or shell, it must go through the existing guards (
security.tsdeny-list, workspace authorization registry, SSRF guard, AI tool approval).
Custom commands do not need to be listed one-by-one in default.json; the capability covers the window. Plugin permissions do.
Command catalog
The commands registered in src-tauri/src/lib.rs are grouped below by module. Names are the Rust function names as seen by the frontend.
PTY (`src-tauri/src/modules/pty/`)
Long-lived interactive terminal sessions.
pty_open- create a new PTY sessionpty_write- send input bytes (text or control sequences)pty_resize- resize the PTYpty_close/pty_close_all- destroy one or all sessionspty_has_foreground_process/pty_has_foreground_job- detect whether a command is runningpty_shell_name/pty_list_shells- shell detection and enumeration
Output streams from pty_open via a Tauri Channel.
File system (`src-tauri/src/modules/fs/`)
Tree
list_subdirs- list subdirectoriesfs_read_dir- read a directory
File
fs_read_file- read file contentsfs_write_file- write file contentsfs_stat- file metadatafs_canonicalize- canonical path
Mutate
fs_create_file/fs_create_dirfs_rename/fs_delete/fs_copy
Watch
fs_watch_add/fs_watch_remove- filesystem change notifications
Search
fs_search- fuzzy file finderfs_list_files- recursive file listing
Grep
fs_grep- content searchfs_grep_interactive- interactive content searchfs_glob- glob matching
Git (`src-tauri/src/modules/git/`)
All git commands are gated through the workspace authorization registry.
git_resolve_repo/git_panel_snapshotgit_statusgit_diff/git_diff_contentgit_stage/git_unstage/git_discardgit_commitgit_fetch/git_pull_ff_only/git_pushgit_log/git_show_commit/git_commit_files/git_commit_file_diffgit_remote_urlgit_list_branches/git_checkout_branch
Shell (`src-tauri/src/modules/shell/`)
Three distinct surfaces:
shell_run_command- one-shot subshell exec for AI toolsshell_session_open/shell_session_run/shell_session_close- persistent agent shell with state across callsshell_bg_spawn/shell_bg_logs/shell_bg_kill/shell_bg_list- long-running background processes with bounded ring-buffer log capture
Workspace (`src-tauri/src/modules/workspace.rs`)
workspace_authorize/workspace_current_dir- the spawn/git/AI cwd authorization registrywsl_list_distros/wsl_default_distro/wsl_home- WSL bridge
Network (`src-tauri/src/modules/net.rs`)
ai_http_request/ai_http_stream- AI HTTP proxy with SSRF guardlm_ping- local-model ping
Secrets (`src-tauri/src/modules/secrets.rs`)
secrets_get/secrets_set/secrets_delete/secrets_get_all- OS keychain access, serviceterax-ai
Agent hooks (`src-tauri/src/modules/agent.rs`)
agent_enable_hooks/agent_hooks_status- install/status terminal coding-agent hooks (Claude Code, Codex, Gemini CLI)
History (`src-tauri/src/modules/history/`)
history_suggest/history_commands/history_record/history_list- shell history integration
Settings window
get_launch_dir- CLI launch directory, drained on first readopen_settings_window- open the separate settings webview (optionaltabdeep-link)
CLI control plane
control_frontend_ready- marks the restored main UI ready for routed CLI actionscontrol_respond- completes a pending UI-bound CLI request
See CLI control plane for the local protocol and packaging model.
Invariants
- The webview must not spawn processes, read files, or make network calls except through the commands above.
- New commands must be registered in
lib.rsand guarded at the boundary (workspace auth, deny-list, SSRF, approval flow). - Plugin permissions must be added to
src-tauri/capabilities/default.jsonif the command uses a plugin API.
See also
TERAX.md- the architecture source of truthdocs/README.md- index of contributor guides- PTY shell integration - how sessions and shell integration work
- Security model - the boundaries every command must respect
File: docs/contributing/testing.md
Testing
This guide elaborates on TERAX.md and CONTRIBUTING.md. If anything conflicts with those files, they win.
Running checks locally
The canonical commands are what CI runs (.github/workflows/ci.yml):
pnpm lint
pnpm check-types
pnpm test
cd src-tauri
cargo clippy --all-targets --locked -- -D warnings
cargo nextest run --locked # CI uses nextestIf you do not have cargo-nextest installed, cargo test --locked is the local fallback. Install nextest with cargo install cargo-nextest.
What must have a test
CONTRIBUTING.md requires a test for any change that touches behavior in these load-bearing paths:
- Shell / terminal spawn (what shell launches, with which cwd, env, and login flags)
- Workspace authorization (both the allow and deny side)
- Git command layer (repo-root resolution, pathspec/argument guards, status parsing)
- Filesystem mutation (atomic writes, symlink handling, no-data-loss on partial failure)
- IPC command surface and AI tool surface
- Pure logic with wide reach (cwd inheritance, tab/split tree transforms, OSC/prompt parsing, command guard)
The bar is real coverage of the contract, not a placeholder. Test the edge, the deny path, the "what happens one level above home".
What does not need a test
UI rendering, themes, syntax-highlight tables, and anything the type-checker already guarantees do not need tests.
Writing a good test
A good test locks the invariant you are relying on. Examples from the codebase:
src-tauri/src/modules/workspace.rsauth_testsverify that an authorized path, a subdir of an authorized root, an unauthorized path, a missing path, and a symlink escape all behave correctly.src-tauri/src/modules/pty/job.rstests verify that dropping the Job Object kills the assigned process tree on Windows.src-tauri/src/modules/pty/session.rstests verify that dropping aSessionkills the child process.src-tauri/src/modules/pty/shell_init.rstests verify shell classification and WSL fish launch specs.src/modules/ai/lib/security.tsis exercised by tests that assert specific paths are refused and that canonicalization catches symlink traversal.
Cross-platform PTY tests
Platform-specific behavior must be gated:
#[cfg(unix)]
fn shell_has_children(shell_pid: u32) -> bool { ... }
#[cfg(windows)]
fn shell_has_children(shell_pid: u32) -> bool { ... }Tests for ConPTY/Job Object belong behind #[cfg(windows)]; tests for Unix PTY lifecycle belong behind #[cfg(unix)]. Do not assume a helper that works on one platform works on the other.
Security function tests
When testing src/modules/ai/lib/security.ts or the Rust equivalents, cover:
- The literal path is refused.
- The canonicalized path is re-refused (symlink case).
- Case variants match on case-insensitive filesystems.
- NTFS alternate data streams and trailing dot/space variants are normalized.
- Write-only deny prefixes block writes but allow reads where appropriate.
Invariants
- A local fix with global blast radius must be caught by a test; review alone is not enough.
- Test the deny path and the edge, not just the happy path.
- Keep platform-specific tests behind the right
#[cfg(...)]gate.
See also
TERAX.md- the architecture source of truthCONTRIBUTING.md- quality bar, project layout, how to contributedocs/README.md- index of contributor guides
File: docs/README.md
Terax contributor documentation
This directory holds long-form contributor and maintainer guides. TERAX.md at the repo root is the living architecture doc and the source of truth; these guides elaborate on specific areas without duplicating it.
If a guide conflicts with TERAX.md, TERAX.md wins.
Getting started
- TERAX.md - the architecture source of truth; read this first
- CONTRIBUTING.md - how to contribute, quality bar, project layout
Architecture guides
- Two-process model and IPC command reference - Rust owns all OS access; the webview talks through
invoke(). Command catalog and how to add a new command. - PTY shell integration - PTY sessions, shell init scripts, OSC 7 / 133, ConPTY, SPAWN_LOCK, Job Object, WSL.
- Security model - deny-list, SSRF guard, workspace authorization, AI tool approval, IPC allowlist, OSC trust, keychain handling.
- AI subsystem - providers, agent, sub-agents, sessions, composer, tools, edit diffs, live context bridge. Includes a walkthrough for adding a new provider.
- Terminal renderer pool - slot pooling, the DormantRing, and the never-serialize-mid-command invariant.
- CLI control plane - bundled CLI, authenticated local protocol, caller targeting, packaging, and current platform limits.
Contributing guides
- Testing - the testing contract, how to run checks, and what makes a good core-subsystem test.