terminal-tools — foundations

SkillDev tools

This skill teaches your AI how to use terminal tools the right way before it starts running commands. Once added, your AI can execute shell commands with proper handling of long-running jobs, output that gets cut off, and exit codes that look like failures but aren't.

Available today. Use it from your connected AI after setup.

Add the skill, then give your AI any task that involves running terminal commands. It will apply the correct patterns for foreground and background execution as it works.

Then ask your AI: use the terminal-tools — foundations skill

What your AI can do with it

  • Run shell commands with correct handling of results and exit codes
  • Move commands that take longer than 30 seconds into the background automatically
  • Check on background jobs by reading their logs while work continues
  • Read structured results including exit codes, output, and warnings
  • Recognize when output was cut off by size limits and retrieve the rest
  • Avoid false alarms from exit codes that don't actually mean failure

What this skill tells your AI

The instructions your AI receives, as published by aden-hive/hive in core/framework/skills/_preset_skills/terminal-tools-foundations/SKILL.md and read by ahel’s review.

These tools give you a real terminal: foreground exec with smart envelopes, background jobs with offset-based log streaming, persistent PTY shells, and filesystem search. Bash-only on POSIX.

Tool preference (read first)

The terminal is your file system: reading, writing, editing, and searching files all go through terminal-tools. Reach for a higher-level tool only where it clearly fits (browser, web search). Terminal tools default their cwd/path to your session workdir when you omit it — relative paths Just Work; pass an absolute path to operate elsewhere.

  • Reading filesterminal_exec("cat PATH") (page large output with terminal_output_get)
  • Editing filesterminal_exec("sed -i ...") / awk, or rewrite the whole file with a heredoc
  • Writing files → heredoc: terminal_exec("cat > PATH <<'EOF' ... EOF")
  • Searchingterminal_rg (content / regex grep) and terminal_glob (find files by name)
  • Browser / web pagesgcu-tools.browser_* for rendered pages — NOT terminal_exec("curl ...")
  • Web searchhive_tools.web_search — NOT scraping
  • System operations (process exec, jobs, PTYs) → terminal-tools. This is its territory.

The standard envelope

Every spawn-style call (terminal_exec, the auto-promoted job state) returns this shape:

{
  "exit_code": 0,                    // null when auto-backgrounded or pre-spawn error
  "stdout": "...",                   // decoded, truncated to max_output_kb (default 256 KB)
  "stderr": "...",
  "stdout_truncated_bytes": 0,       // > 0 means more is in output_handle
  "stderr_truncated_bytes": 0,
  "runtime_ms": 42,
  "pid": 12345,
  "output_handle": null,             // "out_<hex>" when truncated — paginate with terminal_output_get
  "timed_out": false,
  "semantic_status": "ok",           // "ok" | "signal" | "error" — read THIS, not just exit_code
  "semantic_message": null,          // e.g. "No matches found" for grep exit 1
  "warning": null,                   // e.g. "may force-remove files" for rm -rf
  "auto_backgrounded": false,
  "job_id": null,                    // set when auto_backgrounded=true
  "shell_kind": "bash"               // interpreter that ran it: "bash" | "powershell" | "cmd" | "direct"
}

Auto-promotion (the core mental model)

terminal_exec runs commands in the foreground until the auto-background budget (default 30s) elapses. Past that point, the process is silently transferred to a background job and the call returns immediately with:

{ "auto_backgrounded": true, "exit_code": null, "job_id": "job_<hex>", ... }

When you see auto_backgrounded: true, pivot to polling. The job is still running:

terminal_job_logs(job_id, since_offset=0, wait_until_exit=true, wait_timeout_sec=60)
  → blocks server-side until the job exits or the timeout, returns logs + status

You're not failing — you're freed up to do other work while the long task runs.

To force pure-foreground (kill on timeout_sec), pass auto_background_after_sec=0. Use this when you genuinely don't want a background job (small commands where promotion would surprise you).

Semantic exit codes — read semantic_status, not raw exit_code

Several common commands use exit 1 for legitimate non-error states:

Commandexit 0exit 1
grep / rgmatches foundno matches (not an error)
findsuccesssome dirs unreadable (informational)
diffidenticalfiles differ (informational)
test / [truefalse (informational)

For these, semantic_status will be "ok" even when exit_code == 1, with semantic_message describing why ("No matches found"). For everything else, semantic_status defaults to "ok" on 0 and "error" on nonzero.

Rule: always check semantic_status first. Only fall back to exit_code when you need the exact number (e.g. distinguishing make errors).

Destructive warnings — re-read your command

The envelope's warning field is set when the command matches a known destructive pattern (rm -rf, git push --force, git reset --hard, DROP TABLE, kubectl delete, terraform destroy, etc.). The command still ran — the warning is informational. Use it as a "did I mean to do that?" prompt before trusting subsequent steps that depend on the side effect.

If a warning appears unexpectedly, stop and verify: was the destructive action intended, or did a path/glob slip in?

Output handles — never lose output

When stdout_truncated_bytes > 0 or stderr_truncated_bytes > 0, the inline output was capped at max_output_kb (default 256 KB). The full bytes are stashed under output_handle for 5 minutes. Paginate with:

terminal_output_get(output_handle, since_offset=0, max_kb=64)
  → { data, offset, next_offset, eof, expired }

Track next_offset across calls. If expired: true, re-run the command (the handle's TTL has lapsed).

The store has a 64 MB cap with LRU eviction. For huge outputs, prefer terminal_job_start + terminal_job_logs polling (4 MB ring buffer per stream, infinite total throughput).

Bash, not zsh — even on macOS

On POSIX, terminal_exec and terminal_pty_open always invoke /bin/bash (on Windows see the section below). The user's $SHELL is ignored. Explicit shell="/bin/zsh" is rejected with a clear error. This is a deliberate security stance, not aesthetic — zsh has command/builtin classes (zmodload, =cmd expansion, zpty, ztcp, zf_*) that bypass bash-shaped checks. The terminal-tools-pty-sessions skill explains the implications for PTY sessions specifically.

ZDOTDIR and ZSH_* env vars are stripped before exec to prevent zsh dotfiles leaking in. Bash dotfiles still apply when invoked interactively (e.g. PTY sessions use bash --norc --noprofile to keep things predictable).

Windows — check shell_kind before assuming bash

On Windows the shell is resolved in priority order: Git Bash → PowerShell → cmd. Which one ran your command is reported in the envelope's shell_kind field. Bash is only available if Git for Windows is installed; otherwise you land in PowerShell (or cmd as the floor). Read shell_kind and adapt — bash idioms silently break in the others:

You wrotebashpowershellcmd
cat / ls✓ (aliases)✗ (type / dir)
grep / sed / GNU find
a && b✗ in PS 5.1 (use ;)
2>/dev/null2>$null2>nul
single-quoted 'args'✗ (use "...")

Practical rule: if shell_kind != "bash", prefer commands that are portable (a bare program name + args, e.g. node x.js, python -m pip install ...) or write the PowerShell/cmd-native form. Don't assume coreutils. PTY sessions (terminal_pty_*) are POSIX-only and return an "unsupported on Windows" error.

Paths under shell_kind: "bash" on Windows (Git Bash): backslashes are escape characters, so a Windows path passed verbatim gets mangled (cat C:\Users\me\x → bash reads C:Usersmex). Use forward slashes (C:/Users/me/x, which Git Bash accepts) or the MSYS form (/c/Users/me/x). Quoting a backslash path in single quotes also preserves it (cat 'C:\Users\me\x').

Pipelines and complex commands

Pipes (|), redirects (>, <, >>), conditionals (&&, ||, ;), and globs (*, ?, [) are detected automatically. You can pass them with the default shell=False and the runtime will transparently route through /bin/bash -c and surface auto_shell: true in the envelope:

terminal_exec("ps aux | sort -k3 -rn | head -40")
  → { exit_code: 0, stdout: "...", auto_shell: true, ... }

For simple argv commands (no metacharacters) shell=False is faster and direct-execs the binary. For commands with shell features but no metacharacters that the detector catches (rare — exotic bash builtins, here-strings), pass shell=True explicitly:

terminal_exec("set -e; complicated bash logic", shell=True)

Quoted strings work either way — the detector uses shlex.split which handles "quoted args with spaces" correctly.

When to use what (cheat sheet)

NeedTool
One-shot command, ≤30sterminal_exec
One-shot command, might be longerterminal_exec (auto-promotes)
Long-running job from the startterminal_job_start
State across calls (cd, env, REPL)terminal_pty_open + terminal_pty_run
Search file contents (any path)terminal_rg
Find files by name/glob (any path)terminal_glob
Retrieve truncated outputterminal_output_get
Tree / stat / duterminal_exec("ls -la"/"stat foo"/"du -sh path")
HTTP / DNS / ping / archivesterminal_exec("curl ..."/"dig ..."/"tar xzf ...")

See references/exit_codes.md for the full POSIX + signal-induced + semantic catalog.

Signals

GitHub stars
11k
Forks
6k
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
hive-terminal-tools-foundations
Source
github.com/aden-hive/hive