Updated: 47 minutes ago

Essential Terminal Tools for the AI Era | TUI & CLI for Humans and AI Agents

🗺 Map

🗺 Map
🔎 Find & Read Does this file exist, and what's in it?
fd Fast file finder, respects .gitignore.
ripgrep Recursive text search, skips binaries.
eza ls with icons, git status, tree view.
bat cat with syntax highlight, quiet on pipes.
fzf Fuzzy finder for lists and history.
yazi Terminal file manager, image previews.
defuddle Extract article text from noisy HTML.
obscura Headless browser: fetch, scrape, serve.
🧬 Inspect & Query Turn data into answers.
jq Slice JSON responses to the fields needed.
yq jq for YAML, XML, TOML and more.
fq Inspect binary formats: MP4, PNG, PCAP.
jnv Interactive JSON navigator (human-only).
hurl Run HTTP requests from plain-text files.
gh GitHub CLI: PRs, issues, releases.
✏️ Edit & Review Edit files, review what changed.
micro Small terminal editor, mouse support.
edit Microsoft's tiny terminal editor.
delta Readable git diffs, side-by-side style.
lazygit Full git TUI (human-only).
lazydocker Docker TUI: logs, stats, exec (human-only).
📊 System & Network What is actually happening on this machine?
dust Disk usage, biggest folders first.
procs Readable ps with tree and ports.
witr Who owns this port or PID, then kill.
bottom System monitor graphs (human-only).
bandwhich Bandwidth per process (human-only).
trippy Traceroute with live hops, needs root.
⚙️ Build & Verify Environments, linting, benchmarks.
uv Fast Python installs and environments.
ruff Python lint and format in milliseconds.
hyperfine Benchmark commands, with statistics.
🗺️ Navigate & Multiply Move faster, remember more, multiply sessions.
zoxide Jump to frecent directories by name.
atuin Shell history sync (human-only, never agents).
starship Fast informative shell prompt.
zellij Terminal multiplexer, layouts and plugins.
pastel Manipulate and preview colors in terminal.

🩺 The forty-kilobyte problem

Your agent is one command from the answer. It runs the wrong one: cat config.json . The file hands back forty kilobytes of settings nobody asked for.

Nothing fails. No red text, no error, no retry. Every turn after that pays those forty kilobytes again, until somewhere the agent starts guessing. The command it should have run is four words long:

cat config.json # 40 KB — every later turn pays for them
jq '.logging' config.json # the answer, and nothing else

That gap is the subject of this whole post. A human reads past the noise. An agent can't; it pays for every byte on every turn.

Your terminal is already a toolbox packed with thousands of development tools—from familiar, OS-bundled utilities like wget and curl to specialist tools such as Ghidra and Aircrack-ng . Some of those are dangerous in human hands. Hand them to an AI agent that chains commands at machine speed and the risk changes shape: a command a human runs once, after thinking, becomes a loop nobody is watching. An agent doesn't need more tools. It needs the right ones, plus the guardrails that stop a confident wrong answer from becoming a destructive one. The tools shrink what each answer costs; the guardrails stop the destructive one.

The terminal is no longer just a programmer's workspace. Non-programmers use it too, while AI agents can reach across an entire system through terminal interfaces—OpenCode's TUI, Claude, Codex CLI, and similar tools. This post brings together the CLI tools worth having for both humans and agents. Every tool you install also expands your attack surface , so keep the toolbox updated; in the agent era, that maintenance is part of the job.

Every tool below comes with a recording of a real session. Some are optimized for human TUI interaction; many expose interfaces an agent can use directly; a few are shipped as agent defaults (like Hermes Agent). Used in the right workflow, they reduce the bytes each answer costs, which buys more useful turns before the context window fills.

Know what your agent is calling and why. You are responsible for what it does. If you do not understand a tool—or why the agent is asking for access—do not grant it. Used carelessly, a tool with system-level reach can harm your machine or personal files.

Every installer here downloads software from the internet, and some commands run as root . Read a command before you paste it, and read the agent rules before letting an agent run unattended. The genuinely risky cases are flagged inline.


🪜 Where to start

Ninety seconds, then the reference. Read the general rules once — they are the load-bearing part of this post, and everything else is detail. Then, in this order:

  1. The core. fd , ripgrep , jq , yq , hyperfine . No TUI, no root, no config. Copy the one-liner and stop.
  2. The agent layer. rtk and procs . Add the rest when a real task asks.
  3. The cheat sheet. Paste that table into your agent's system prompt.

This post is a starting point, not a replacement for any tool's documentation. Flags, options and keybindings belong to --help and the upstream project — the only shortcuts written out here are the ones where guessing wrong costs you data.


📦 Installing everything

🐧 Linux & WSL — one command

Installs everything your distribution already packages, skips what it does not have, and fixes the one naming trap ( fd-find → fd ) along the way.

Ubuntu / Debian / WSL — the packaged subset

sudo bash -lc 'set -u; apt-get update -qq; for p in ripgrep bat fd-find jq fzf zoxide lazygit git-delta hyperfine gh starship btm pastel procs micro fq; do if apt-get install -y --no-install-recommends "$p" >/dev/null 2>&1; then printf " [ ok ] %s\n" "$p"; else printf " [ -- ] %s (not in your repos)\n" "$p"; fi; done; if command -v fdfind >/dev/null; then ln -sf "$(command -v fdfind)" /usr/local/bin/fd; fi; if command -v batcat >/dev/null; then ln -sf "$(command -v batcat)" /usr/local/bin/bat; fi; echo; for c in fd rg bat jq fzf zoxide lazygit delta hyperfine gh starship btm pastel procs micro fq; do command -v "$c" >/dev/null && echo " $c"; done'

Not in stable Debian, so take them from GitHub releases instead: zellij , jnv , rtk and the Go yq ; witr and pastel are sid-only, though cargo install pastel works on any stable box. Check the published SHA-256 digest before running a downloaded binary.

Prefer not to run a loop? The master install table gives you one row per tool, linked to its section — copy only what you want.

🪟 Windows & PowerShell — one command

The Linux bundle does not apply to Windows. Scoop is the practical route here because scoop install accepts the whole list in one command, and 33 of the 34 tools in this post are available from a Scoop bucket:

PowerShell — the whole toolbox, one command (Scoop)

scoop bucket add extras # lazygit is the only tool not in the Main bucket
scoop install ripgrep bat fd eza fzf jq yq fq jnv hurl gh micro edit delta lazygit lazydocker dust procs witr bottom bandwhich trippy uv ruff hyperfine zoxide atuin starship zellij pastel rtk yazi obscura

Two lines there matter. lazygit is not in Scoop's Main bucket, so scoop bucket add extras has to come first or that one package fails. And ripgrep , bat , obscura , rtk and starship are MSVC builds that need the Visual C++ runtime:

PowerShell — the vcredist runtime they need

scoop install extras/vcredist2022

Scoop handles the obscura two-binary problem for you; on Linux you have to put both on PATH yourself.

The one tool Scoop does not have is defuddle , which is an npm package and needs Node.js:

PowerShell — the last tool, which is an npm package

npm install -g defuddle

Or skip installing it and run it on demand: npx defuddle parse <file> — nothing installed, which is the safer default on a machine you don't own.

Don't have a third-party package manager? winget ships with Windows 10/11, but there is deliberately no frozen ID list here. Those identifiers drift as publishers rename and re-register, and winget install fails on an unknown one—a list that is even slightly out of date can abort the whole batch partway through. Look each one up as you go:

PowerShell — winget, one tool at a time

winget search ripgrep
winget install --id BurntSushi.ripgrep.MSVC -e --silent `
--accept-package-agreements --accept-source-agreements --disable-interactivity

Neither requires elevation for a per-user install. If a package fails, skip it—every tool here has a portable-binary fallback, and none depends on another tool in the list.

One Windows-only trap can make a perfectly good install look broken: in Windows PowerShell 5.1 , curl is an alias for Invoke-WebRequest , not curl.exe , and it shadows the real binary. A command copied from a Linux guide like curl -o out.json https://… fails with a parameter error that has nothing to do with the tool you are installing. (PowerShell 7+ dropped the alias, so this only bites 5.1.) Call the real binary by full name to sidestep it either way:

PowerShell — the real curl, not the alias

curl.exe -o out.json https://example.com

The scoop install line above and the Windows column of the master install table both include atuin , because this post is written for people. Drop atuin from either list before scripting an install — it is the one tool in this post that is never safe to automate, and the reason is in its section . Everything else in those two lists is fine. The agent column marks which rows to keep.

Every tool in that list was installed this way on a Windows Server 2025 x64 machine and each one then run with --version , so the set is verified working rather than merely verified installable — on a Server SKU, the harder case, since none of these tools can assume a desktop session. The demos are Linux; the Windows column exists so a reader on either OS can get the same set running. Expect the Windows versions to differ from the ones in the captions.

Run that --version check anyway, because a successful install does not guarantee that the binary you just installed is the one that actually runs. On the machine above, an older starship 1.21.1 at C:\Program Files\starship\bin sat ahead of Scoop's 1.26.0 on PATH , so the command was answering for a different file. Ask Windows which file it would actually run:

PowerShell — which binary is answering, and what else is on PATH?

where.exe starship
& "$env:USERPROFILE\scoop\apps\starship\current\starship.exe" --version

where.exe lists every match in resolution order, so the first line is the one Windows will resolve first. If that is not the copy you just installed, something else is shadowing it. scoop status will not catch this: on that same machine it cheerfully reported Everything is ok! while a completely different starship handled the command. Scoop also does not always create a shim; for starship it adds the app directory to PATH itself, a second way for an older copy to end up in front.

✅ Verify what actually landed

A mismatched package name versus binary name is one of the easiest ways to think an install failed, and fd-find / fdfind , git-delta / delta , trippy / trip , bottom / btm , and the yq collision all come from this class of problem.

🐧 Linux / WSL

Print OK or MISSING for every command on this page

for cmd in fd rg eza bat jq yq fq jnv hurl gh micro edit delta lazygit lazydocker \
dust procs witr btm bandwhich trip uv ruff hyperfine zoxide atuin \
starship zellij pastel fzf yazi rtk; do
printf "%-12s : " "$cmd"
command -v "$cmd" >/dev/null && echo "OK" || echo "MISSING"
done

🪟 Windows / PowerShell

The same audit, Windows edition

$cmds = 'fd','rg','eza','bat','jq','yq','fq','jnv','hurl','gh','micro','edit','delta',
'lazygit','lazydocker','dust','procs','witr','btm','bandwhich','trip','uv','ruff',
'hyperfine','zoxide','atuin','starship','zellij','pastel','fzf','yazi','rtk'
$cmds | ForEach-Object {
'{0,-12} : {1}' -f $_, $(if (Get-Command $_ -ErrorAction SilentlyContinue) { 'OK' } else { 'MISSING' })
}

📇 Master install table

The Agent column repeats the verdict from each tool's own label. no means never put that row in an install list an agent will run. The Use instead column tells you what to reach for—and for a cond tool, it gives the exact form the agent must use. Every tool name links to its section, and this is the only place where install commands appear.

pastel , dust , eza , jnv , bandwhich and atuin fall back to cargo install , which assumes a Rust toolchain on the machine . Take the release binary instead if you'd rather not install one - each publishes a digest, and verify it rather than trusting the filename.

# Tool Agent Use instead 🐧 Linux (bash) 🪟 Windows (PowerShell)
1 fd yes — apt install fd-find + symlink to fd scoop install fd
2 ripgrep yes — apt install ripgrep scoop install ripgrep
3 defuddle yes — npm install -g defuddle ⚠️ or the release .zip npm install -g defuddle ⚠️ needs Node.js
4 obscura yes curl , for a page with no JavaScript release .tar.gz — extract both obscura and obscura-worker scoop install obscura — installs both binaries
5 jq yes — apt install jq scoop install jq
6 yq yes — GitHub release (not the apt one) scoop install yq
7 fq cond fq -V '.tools' file apt install fq (it's Go, not Rust) scoop install fq
8 hurl yes — apt install hurl scoop install hurl
9 gh yes — apt install gh scoop install gh
10 procs yes — apt install procs scoop install procs
11 witr yes — not in stable Debian - use the release binary scoop install witr
12 uv yes — official install.sh · or pipx install uv scoop install uv · or pip install uv
13 ruff yes — uv tool install ruff scoop install ruff
14 hyperfine yes — apt install hyperfine scoop install hyperfine
15 zoxide no the absolute path apt install zoxide scoop install zoxide
16 pastel cond pastel format hex apt install pastel · or cargo install pastel scoop install pastel
17 rtk yes — release binary (not packaged) scoop install rtk
18 fzf cond fzf --filter=PATTERN apt install fzf scoop install fzf
19 dust yes dust -j -n 20 / cargo install du-dust scoop install dust
20 trippy cond trip <host> -m json release .deb from the GitHub release, binary is trip scoop install trippy
21 eza no ls , or fd --type f apt install eza · or cargo install eza scoop install eza
22 bat no cat apt install bat → binary is batcat scoop install bat
23 delta no git --no-pager diff apt install git-delta → binary is delta scoop install delta
24 jnv no jq cargo install jnv scoop install jnv
25 yazi no fd , or ls apt install yazi scoop install yazi
26 micro no write the file directly apt install micro scoop install micro
27 edit no write the file directly release tarball → /usr/local/bin/edit scoop install edit
28 lazygit no git status , git --no-pager diff apt install lazygit scoop install lazygit
29 lazydocker no docker ps , docker logs official install_update_linux.sh scoop install lazydocker
30 bottom no procs , free -h , ss -s apt install btm (binary is also btm ) scoop install bottom
31 bandwhich no ss -tnp cargo install bandwhich scoop install bandwhich
32 starship no nothing — it is the prompt official install.sh scoop install starship
33 zellij no nothing - it is a UI release binary (not in apt) · or cargo install zellij scoop install zellij
34 atuin no nothing — never install official install.sh · or cargo install atuin scoop install atuin

🎬 About the recordings

Every demo is a real capture of the tool running, not an animation. They were recorded on 2026/09/29 , and the version shown under each video is the one that was on screen, verified with the tool's own --version flag.


🔎 Category 1 — Find & Read

Does this file exist, and what's in it? The two questions an agent asks most, and the two it is worst at answering unaided. The last tool here starts from the other end — the same two questions, asked of a URL.

📂 fd

fd v10.5.0 — filtering the tree by file extension. Ran fd -e py .

A fast, friendly file searcher: find without the ceremony. Regex by default, .gitignore -aware, colourised.

fd notes # regex-match names below here
fd --glob '*.py' # or switch to glob matching
fd -e py # only .py files
fd --type d --max-depth 3 src # bound it: three levels, directories only
fd -0 # NUL-separated, so paths with spaces survive

✅ Do

  • Use fd to find a path ; use rg to find a string . Confusing the two is the most common agent misuse.
  • Always narrow with --type , --extension , --max-depth or --changed-within . The default is a recursive walk of everything below the current directory, which is a wall of text and a burned context window.
  • Target the directory you mean: fd <pattern> <dir> beats cd then a bare fd .

🚫 Don't

  • Don't use rg --files for filenames when fd does it in one call.

🔍 ripgrep ( rg )

rg v15.2.0 — one search across a tree, showing file, line and match. Ran rg 'Hello' . .

The recursive text search engine, and the right tool for "search this whole project". What an agent actually gets from it is scope and bounding: .gitignore and binaries are skipped by default, so results are the files you meant rather than every file on disk, and -l , -c , -g and -t cap the output. Parallel traversal is a real win — a secondary one, not the headline.

rg 'Hello' . # one search across the whole tree
rg -l "os.environ" # filenames only: tiny output
rg -c "TODO" # counts per file: cheapest survey

✅ Do

  • Search narrow, then widen. rg -g '*.ts' "token" src/ before rg "token" . ; scope to the relevant subdirectory and filter by type with -g / -t .
  • Use -l (filenames) or -c (counts) when you only need where or how much . The context difference is enormous.

🚫 Don't

  • Don't search .git , node_modules , target , dist , .venv , __pycache__ — .gitignore doesn't always cover generated directories.
  • Don't assume a missing hit means a missing thing. rg does not read inside PDFs, images or compiled artefacts.

📋 eza

eza v0.23.5 — a one-level tree that fits the frame, then the same listing largest-first. Ran eza -la --icons=always --git --tree -L 1 , then --sort=size --reverse .

A modern replacement for ls — colour, file-type icons, Git status columns and a tree view.

eza -la --icons=always --git --tree -L 1 # one level, with Git status
eza --long --sort=size --reverse # what is eating my disk here?

🦇 bat

bat v0.26.1 — a Python file with a line-number gutter. Ran bat --style=numbers app.py .

cat with syntax highlighting, line numbers, a Git-change gutter and a header. The nicest way for a human to read a source file.

bat main.py # syntax highlighting + line numbers
bat --diff # only the lines that changed

🎯 fzf

https://github.com/junegunn/fzf agent: yes, with --flag

fzf v0.74.4 — three filenames piped in and matched non-interactively. Ran fzf --filter=app , which returns the match without a TTY.

The general-purpose fuzzy finder. Pipe anything into it, type two letters, get a file.

fd --type f | fzf --filter='\.rs$' --select-1 --exit-0 # non-interactive
fd --type f | fzf --preview 'bat --color=never {}' # the human picker

✅ Do

  • Pair the interactive form with fd ( FZF_DEFAULT_COMMAND ) and bat ( --preview ) for a good file opener.

🚫 Don't

  • Don't leave find . undefined fzf in an alias that a non-interactive shell might end up running.

🗂️ yazi

yazi v26.9.1 — four file types previewed in sequence, all syntax-highlighted by yazi's own built-in previewer. Ran yazi , then Down through the entries, then q .

A blazing-fast terminal file manager with async I/O, archive browsing and type-aware previews. The closest thing to a GUI file browser that stays in the terminal.

yazi # browse the current directory
yazi ~/Downloads # or start somewhere else

Previews are the headline. Two external helpers make it great:

sudo apt install chafa # render images inline
sudo apt install glow # terminal renderer for .md
mkdir -p ~/.config/yazi # config dir, created if absent
cat > ~/.config/yazi/yazi.toml << 'EOF' # [plugin]: prepend a preview rule
[plugin] # ...matched on mime
prepend_previewers = [
{ mime = "text/markdown", run = "glow" },
]
EOF

🧽 defuddle

defuddle v0.19.2 — a deliberately noisy page, then the same page after extraction: 2107 bytes → 678. Ran defuddle --version , wc -c noisy.html , defuddle parse -m noisy.html undefined wc -c , defuddle parse -m noisy.html , then defuddle parse -m -f noisy.html .

Strips a page down to the article. Navigation, cookie banners, ad slots, related-article sidebars, comment threads, footers and the <script> tags all go.

It filters HTML you already have — from a file, stdin or curl . Give it an http:// argument and it fetches that URL itself. And -f adds YAML frontmatter, so the page's <meta> description — buried in the raw HTML — comes back as a field instead of being thrown away with the noise.

✅ Do

  • Pipe it. curl -L URL undefined defuddle parse -m skips the temp file entirely.
  • Use -p to pull one field ( title , description , author ) without printing the body at all.

🚫 Don't

  • Don't hand it a URL you didn't choose. It has no private-network guard, so an agent given an attacker-supplied URL will fetch it. Same bug class obscura is careful about.
  • Trust it on a page with nothing server-rendered. Extraction is heuristic, and the author ships it as a work in progress.

🥷 obscura

obscura v0.2.3 — a page whose body is written by an inline <script> : curl returns root">Loading , obscura returns the rendered report. Then the batch side, two URLs at a time. Ran obscura --version , curl -s http://127.0.0.1:8099/ undefined grep -o 'root.*Loading' , obscura fetch --allow-private-network --dump markdown http://127.0.0.1:8099/ , obscura scrape https://example.com https://www.iana.org/help/example-domains , then obscura scrape --eval 'document.title' --format json https://example.com https://httpbin.org/html .

A headless browser written in Rust that runs real JavaScript in V8 and speaks CDP, so Puppeteer and Playwright connect to it unchanged. No Chromium and no Node: a ~70 MiB binary, ~30 MB resident, against Chrome's 300 MB download and 200 MB resident.

Its whole reason to exist is the page curl can't answer. On a JS-heavy site curl returns the empty shell the script is written to fill — the clip's root">Loading is exactly that — so nothing short of a browser ever sees the content. Reach for curl when the HTML is already there; reach for this when it isn't.

Install both binaries, not one. The tarball ships obscura and obscura-worker side by side, and scrape shells out to the second — so a first install that put only obscura on PATH works perfectly for fetch and then fails at the batch step with a bare worker binary not found . On Linux that means unpacking the tarball and putting both on PATH yourself; scoop install obscura does it for you on Windows.

What it's for

  • Reading one page that only exists once JavaScript has run — --dump markdown for the text, --dump links for the url<TAB>title graph, --dump assets for the sub-resource list as NDJSON.
  • obscura scrape url1 url2 … for the same across a batch, in parallel. Its JSON carries total_urls , concurrency , a worker id per result and per-URL time_ms , and --eval 'document.title' runs your own expression against every URL at once — which is the reason an agent reaches for it.

✅ Do

  • Leave the private-network protection alone. Loopback and internal addresses are blocked by default, so an agent handed a URL can't be tricked into fetching your cloud metadata endpoint.

🚫 Don't

  • Add --allow-private-network for anything you didn't start yourself. It disables that SSRF protection, which on a shared network is a genuine attack surface.
  • Put obscura serve or obscura mcp on a non-loopback bind. Since 0.2.3 those control ports demand a ≥32-byte bearer token, and a loopback bind is the only reason you'd be holding that key at all.

🧬 Category 2 — Inspect & Query

These tools turn data into answers . They let an agent inspect what is inside a file without writing a parser—and, used carelessly, they can make a wrong answer look convincing.

🔣 jq

jq-1.7 — printing a JSON file, then its top-level keys, then extracting one field from every item. Ran cat inventory.json , then jq keys , then a raw jq -r over .items[].id .

jq does more work for an agent than anything else here, because it is what stops a 40 KB API response becoming 40 KB of context. It has its own filter language, and the two are related but not interchangeable - see the note under yq before borrowing a filter.

jq 'keys' file.json # inspect the schema first
jq -r '.items[].id' results.json # -r = raw strings, not quoted JSON

✅ Do

  • Pipe large JSON through jq before it reaches the agent context. Slicing a response to the fields you need is the largest single reduction in this post.

🚫 Don't

  • Don't assume a schema. Run jq 'keys' first — a wrong path returns null and the agent will build on that.
  • Don't mutate important JSON blindly. Write to a new file, verify, then move it.
  • Don't use jq on HTML or a log line. That's rg .

📄 yq

yq v4.53.6 — the whole config, then one subtree, then a single scalar. Ran cat config.yaml , then selected .server , then drilled to .server.port .

A structured-data processor with its own expression engine, reading and writing YAML, JSON, XML, TOML, CSV, properties and ini. It is jq- shaped , not jq — different engine, different operators.

yq '.server' config.yaml # lift one subtree out
yq -o=json '.services' compose.yml # as JSON, for a program
yq -i '.services.api.replicas = 3' compose.yml # structural edit, never sed

✅ Do

  • Prefer yq -i over sed for YAML every time . A sed that changes one replica count can re-quote the whole file into something the parser then rejects.
  • Inspect the subtree before editing, and read the file back after.
  • Use -o=json when a program reads the result. -I is --indent , not a document separator, so it is not the flag for one-document-per-line.
  • Keep in mind that yq has a system operator, and that it is disabled by default — the real form is system("<exe>") , and it only runs with --security-enable-system-operator . Even then yq treats the whole string as a single executable path, so arguments do not split. Never let an expression you did not write reach it. --security-disable-env-ops and --security-disable-file-ops disable the related built-ins.

🚫 Don't

  • Don't apt install yq and assume you got this one. On Debian/Ubuntu yq is a completely different Python tool with jq syntax. Confirm with yq --version .
  • Don't write "cmd" undefined system — that form is a syntax error , not a shorthand. And don't expect arguments to work: system("cat /etc/hostname") fails with a fork/exec error, because yq looks for one executable literally named cat /etc/hostname . system("hostname") , system("cat") and system("id") do run.
  • Don't rewrite a whole YAML file to change one field. Diff the result.

🔬 fq

https://github.com/wader/fq agent: yes, with --flag

fq v0.18.0 — a full JSON document, then one array, then a single key. Ran cat sample.json , then selected .tools , then selected .version .

A query engine for binary and structured data, with a jq-like language. Where jq reads JSON, fq reads everything — PCAP, ELF, PNG, MP4, protobuf, gzip. SQLite is not among them. It is a fork of gojq, so it is closer to jq than most, but still not jq.

fq -V '.tools' sample.json # -V = JSON output, not a hex dump
fq -V '.boxes[0].type' clip.mp4 # ...and formats jq cannot read at all

✅ Do

  • Use it to inspect a format before deciding you need a custom parser. It often saves you writing one.
  • Always pass -V . Without it you get a colourised, truncated human view; add -M and -o array_truncate=0 -o string_truncate=0 for a lossless read.
  • Query only the portion you need, and work on a copy when exact bytes, hashes or offsets matter.

🚫 Don't

  • Don't port a jq filter across unchanged. Arguments are separated by ; not , , and the default output function is a display, not JSON.
  • Don't treat its output as a semantic verdict. It decodes bytes; it doesn't know intent.

🧭 jnv

jnv v0.7.1 — the full file, a live query, and an invalid path reported rather than swallowed. Ran cat sample.json , then jnv sample.json , typed .tools , appended a bad key, then q .

An interactive JSON navigator: a live jq prompt that shows the result as you type. A bad path is reported immediately instead of quietly yielding null , so you catch a wrong key early.

jnv file.json
cat data.json | jnv

🧪 hurl

hurl v8.0.1 — the test as source, then the same test passing its assertions. Ran cat test.hurl , then hurl --test test.hurl .

A declarative HTTP request runner. You write the request, the expected status and the assertions in a plain-text file — and the file becomes a test.

GET https://httpbin.org/get
HTTP 200
[Asserts]
jsonpath "$.url" == "https://httpbin.org/get"
hurl --test test.hurl

✅ Do

  • Use it instead of regenerating one-off curl commands on every check. The test file is the thing worth keeping; put it in CI.
  • Use captured values for multi-step flows. The syntax is name: <query> — so token: jsonpath "$['access_token']" , referenced later as {{token}} . A request header is a plain Name: value line in the entry, not header "..." ; that form is a capture or assert query.

🚫 Don't

  • Don't treat it as a load tester. It's a request runner — though note --repeat -1 is an infinite loop, so keep the repetition flags off unless repeating is the task.
  • Don't accept HTTP 200 as proof of success. A failing business operation often still returns 200 — that's what [Asserts] is for. Don't skip them, and don't pass --no-assert .
  • Don't point it at a service you don't control without asking.

🐙 gh

gh v2.101.0 — one repository as JSON with four requested fields. Ran gh repo view charmbracelet/vhs --json name,description,stargazerCount,url .

GitHub's official CLI. Issues, pull requests, releases, Actions — and, most usefully, authenticated, structured API access without hand-writing headers.

The pattern that matters: ask for exactly the fields you want

gh auth login
gh repo view charmbracelet/vhs --json name,description,stargazerCount,url

✅ Do

  • On commands that support it, pass --json with an explicit field list — that is gh's structured-output mechanism, and a typo fails loudly with the list of valid fields.
  • Put the token in the environment ( GH_TOKEN ), not on the command line, where it lands in shell history and the agent's transcript. Don't commit one, paste one into a command, or print $GH_TOKEN while recording.

🚫 Don't

  • Don't use gh as a general HTTP client. gh api --hostname does reach GitHub Enterprise and ghe.com tenancy hosts, but it only speaks the GitHub API shape, so it won't fetch an arbitrary non-GitHub API the way curl would.
  • Don't quote a live value like stargazerCount as a fact — it drifts by thousands in a month.

✏️ Category 3 — Edit & Review

📝 micro

micro v2.0.15 — a Python file with highlighting and a status bar, then a clean exit. Ran micro app.py , then Ctrl+Q .

A modern terminal editor with mouse support, multiple buffers and modern keybindings. What you want when the alternative is a full IDE.

micro file.txt
export EDITOR=micro # so crontab -e and friends open something usable

✏️ edit

edit v2.0.0 — the file printed so line numbers are visible, then opened directly at line 2. Ran cat app.py , then edit -g app.py:2 .

Microsoft's small, fast terminal editor with Ctrl+F search built in and no configuration to learn. Its one superpower is the -g goto form — it opens with the cursor already where you want it. Microsoft recommends packaging it as msedit to avoid the name collision.

edit -g app.py:2 # -g is required; a bare app.py:2 is just a filename
sudo apt install libicu-dev # runtime library for Ctrl+F on Linux; the suffix is suite-specific (libicu72 on bookworm)

↔️ delta

delta v0.19.2 — an unstaged diff, syntax-highlighted. Ran git diff piped into delta .

A syntax-highlighting pager for git diff , git log and git blame .

git config --global pager.diff delta # not core.pager
git --no-pager diff | delta --paging=never --color-only

Set interactive.diffFilter for a live highlighted diff inside git add -p .

🐢 lazygit

lazygit v0.65.1 — the Files panel, then the live unstaged diff. Ran lazygit , then Enter to clear the splash, then Down , then q .

The whole Git workflow — status, staging, diffs, branches, stashes, rebase — in one TUI. ? prints the full keybinding set at any time.

🐳 lazydocker

lazydocker v0.25.2 — live container panels, then the detail view of a Docker network. Ran lazydocker , then Down Down , then Right , then q .

A terminal UI over your Docker daemon: containers, images, volumes, networks, live logs and stats. The keybindings sit in the bottom line at all times, so you never have to guess a key.


📊 Category 4 — System & Network

What is actually happening on this machine? This is where an agent that guesses instead of measuring can do real damage.

🌫️ dust

dust v1.2.6 — one level of directory sizes. Ran dust -d 1 .

A more readable take on du : the biggest directories at a glance instead of a wall of 4 KB block counts.

dust -j -n 20 / # the 20 largest, as JSON

✅ Do

  • Use dust -n 20 first — bounded output, and it answers "what's big here?" in a single run. Add -j when a program reads the result, and keep -n too: the row limit bounds the output.
  • Use -d to control depth. A full-depth dust / is a context flood, so don't run one on a machine you don't own.

🚫 Don't

  • Don't reach for -j to make a scan parallel. Threading is -T / --threads ; in dust , -j is --output-json . And don't read a ratio against du as a property of the tool: dust 's speed comes from threads, so it beats du only when cores are free. Pinned to one thread ( -T1 ) it is slower than du , and on an 8-vCPU native host du was 2.3x faster. Use dust for -n bounded output and -j JSON, not for speed.
  • Don't loop du -sh . That's what dust replaced.

🌳 procs

procs v0.14.12 — the process table filtered to one name. Ran procs python .

A modern process viewer: process trees, resource usage, ports, containers and cgroups in one readable table.

procs --json # JSON, one object per process
procs --tree # the first move when something misbehaves

✅ Do

  • Use procs --json when something else reads the result. It bypasses the pager, truncation and colour, which is why it is the flag and the default view is not. Its man page does not list it, so this is easy to miss; if --json is missing, check the CHANGELOG.
  • Use procs --tree as the first move when something behaves strangely. The parent/child shape usually explains the problem on its own.
  • Work up deliberately: procs for a first look, then /proc , ps with explicit columns, ss , systemctl or docker inspect when you need the real answer.
  • Reach for witr when the question is why a process exists, not just what it is.

🚫 Don't

  • Don't kill a process because it appeared in procs . It was there before you started.
  • Don't assume the process owning a port is safe to terminate, or that a process name proves ownership. python3 might be anything.
  • Don't ask for the Env column. It prints another process's full environment, which is where API keys live, and that output lands in your context.
  • Don't combine --json with --only or --tree on v0.14.12 — a skipped column produces invalid JSON. Check that the output parses.
  • Don't replace systemctl status or docker inspect with procs when those are the authoritative source.

🕵️ witr

witr v0.3.3 — a live system daemon traced to its owner, unit file and parent chain. Ran witr cron .

The question is not "what is running?" but " why is it running, and who started it? "—parent, supervisor, unit file, user, full ancestry.

problem -> witr -> identify owner/parent/supervisor -> inspect -> decide -> only then restart/kill/modify

✅ Do

  • Run witr before killing or restarting anything unfamiliar, and especially before freeing an occupied port.
  • Pass a name , not a hard-coded PID. A PID from a minute ago may have exited, and a stale one produces a confident, wrong answer.
  • Read the warnings block. "Running as root" raises the stakes for whatever you do next.

🚫 Don't

  • Don't kill -9 a mysterious process just because it owns port 8000. That is how a running database loses unsaved work.
  • Don't stop a service before establishing whether it's supposed to be running.
  • Don't infer ownership from the process name alone.

🖥️ bottom ( btm )

btm v0.14.9 — live CPU, memory, network and process graphs; the temperature panel is empty on a VM. Ran btm , then q .

A graphical system monitor — htop 's more attractive younger sibling. CPU, memory, disk I/O and network as live graphs, so a sawtooth or a 30-second spike becomes obvious, plus a process list you can sort, filter and act on without memorising ps flags.

btm --basic # CPU, memory, network, disk only

🚫 Don't

  • Don't launch btm to get a number. It is a full-screen TUI that waits for a keystroke, and there is no report mode: read procs , free -h and ss -s instead, or docker stats for containers.

🌐 bandwhich

bandwhich v0.23.1 — per-process bandwidth attribution with connection counts and up/down rates. Ran bandwhich , then Tab to cycle the display view.

A live terminal bandwidth monitor that attributes traffic to the process that owns it—and shows which host or address accounts for the most traffic, rather than only the total.

bandwhich # needs root, or all four capabilities
sudo setcap cap_sys_ptrace,cap_dac_read_search,cap_net_raw,cap_net_admin+ep "$(command -v bandwhich)"
bandwhich --raw -n # the pipeable form, with DNS lookups off

🛰️ trippy ( trip )

trip v0.13.0 — a live hop-by-hop table filling in with loss and latency columns. Ran trip 1.1.1.1 .

traceroute and ping fused into one live view: per-hop loss, jitter and latency, updating as you watch.

trip 1.1.1.1 # the live TUI
trip 1.1.1.1 -m json # the agent: machine-readable

✅ Do

  • Use -m json (or another report mode) anywhere automated, and write the output to a file so the numbers survive the session. Run it long enough to be meaningful; one probe proves nothing.

🚫 Don't

  • Don't claim a network problem from a single transient probe. Repeat the measurement.
  • Don't treat a missing intermediate hop as proof of failure — plenty of routers refuse to answer and the route continues past them.

⚙️ Category 5 — Build & Verify

🐍 uv

uv v0.11.29 — creating a virtualenv, then listing what is installed in it. Ran uv venv , then uv pip list .

An extremely fast Python package and project manager — resolution, virtual environments and installs, typically an order of magnitude quicker than pip .

A project, start to finish

uv venv
uv add requests
uv run python main.py # resolves the right env every time
uvx ruff --version # run a tool without installing it

✅ Do

  • Use uv run <cmd> instead of activating a venv. It removes a whole class of "wrong Python" bugs.
  • Use uvx for one-shot tools — no install, no pollution.
  • Keep uv.lock authoritative for the team via uv add / uv sync .

🚫 Don't

  • Don't mix pip install and uv in the same environment. You'll get dependency state neither tool can reason about.
  • Don't uv pip install into the system Python. That's what the venv is for.

🧹 ruff

ruff v0.16.9 — a lint pass over a single file. Ran ruff check app.py .

A very fast Python linter and formatter in one binary.

ruff check . # the default gate
ruff check --output-format=json . # for a program, not a person

✅ Do

  • Run ruff check after modifying Python, every time. It's fast enough that there's no excuse.
  • Prefer safe automatic fixes over hand-editing lint errors, and respect the project's own ruff config — don't override it to make a warning disappear.

🚫 Don't

  • Don't treat a clean lint as proof the program is correct. Ruff checks style and a class of bugs; not your logic, types or tests.
  • Don't reach for --unsafe-fixes casually — "unsafe" is the tool telling you it may change behaviour.

⏱️ hyperfine

hyperfine v1.20.0 — two commands benchmarked to a mean ± σ and a speedup ratio. Ran hyperfine --warmup 1 'sleep 0.02' 'sleep 0.04' .

Statistical command-line benchmarking. Instead of one time run, it repeats a command enough times to tell you whether the difference is real.

hyperfine --warmup 1 'sleep 0.02' 'sleep 0.04'

✅ Do

  • Benchmark a baseline before optimizing, and use --warmup so cold-start effects don't pollute the comparison.
  • Export the result when you're writing it down: hyperfine --export-json result.json … .
  • Do the significance comparison yourself. The ± σ is the point — it shows how much spread the measurements had, so you can see whether the gap between two means is large next to it, which a bare time cannot. hyperfine reports mean, σ and a speedup ratio and stops there: it has no significance test and will not tell you a difference is real.

🚫 Don't

  • Don't benchmark anything that isn't idempotent. --min-runs defaults to 10, and --prepare runs before each timing run, so a git commit or curl -X POST runs a dozen times. Nothing warns you.
  • Don't compare different workloads as though they were equivalent.
  • Don't optimize based on a difference smaller than the reported σ .
  • Don't use it when performance is irrelevant. It isn't free.

🔖 zoxide

zoxide v0.10.0 — the frecency list, a fuzzy jump, and pwd proving it landed. Ran zoxide query --list , then z mock_env , then pwd .

A cd replacement that learns. It keeps a frecency database — every directory, scored by how often and how recently you visit it.

eval "$(zoxide init bash)" # add to ~/.bashrc
z project # fuzzy substring, not a path

📜 atuin — a human-only tool, and the reason matters

atuin v18.23.0 — the history table, the Ctrl+R search UI with its mode tabs and [GLOBAL] indicator, then the frequency bars from atuin stats . Ran atuin --version , atuin history list , Ctrl+R → git → Esc , atuin search --format '{command}' git , atuin history list --cmd-only --cwd , then atuin stats .

The history table lists clear , cd /srv/api-gateway and atuin's own commands, because those genuinely ran while the clip was recording.

A better shell history: searchable, synced across machines, keyed by directory, exit code and duration. Genuinely excellent for a person, and the single most dangerous tool in this post for an automated agent.

The problem is not that history is private. It is that shell history is a secret store in practice. It accumulates every credential ever typed on a command line:

export GH_TOKEN=ghp_xxxxxxxxxxxx
curl -H "Authorization: Bearer $API_KEY" https://api.example.com/...
psql postgres://admin:hunter2@db.internal/app
mysql -uroot -p'correct horse battery staple' < dump.sql
AWS_SECRET_ACCESS_KEY=wJalr... git push https://user:token@github.com/org/repo.git

Run atuin search and you have every one of those. Register, run atuin sync , and you have a channel that exports your history to another machine. And the search UI runs the selected command on Enter — driving that UI is one keystroke from replaying something destructive.

Two things that make it worse for an agent specifically. Atuin ships atuin hook install opencodeundefinedclaude-codeundefinedcodexundefinedpi , which records your agent's own shell commands into that archive, plus an MCP server and ai.capture_sessions for copying full agent transcripts. And the credential filter, though on by default, is a regex blocklist that upstream itself calls best-effort — it knows common formats and misses one that colour codes split apart.

The rule: if you are an agent, do not install atuin, do not run atuin at all, and do not read the database. If you genuinely need to know what was run, ask the human to paste it.

Install it with the official install.sh , but note that the installer walks you through creating an Atuin Cloud account.

eval "$(atuin init bash)" # then Ctrl+R for fuzzy history
# local-only: auto_sync = false in atuin's config, and never run atuin register

Two different tools, two different shortcuts: z is zoxide (directory jumping) and Ctrl+R is atuin (history search), unless your shell or another history tool took it first. Install both if you like — but know which one you pressed, and know that only one of them is safe to use from a script or an agent.

⭐ starship

starship v1.26.0 — the rendered prompt for the current directory. Ran starship prompt .

A fast, cross-shell prompt that shows your directory, Git branch and status, and active language version in a few milliseconds. Presets give you a good-looking prompt immediately; per-module configuration is there when you want more.

eval "$(starship init bash)"
starship explain # why is my prompt slow?

🪟 zellij

zellij v0.45.1 — the default bindings people usually get wrong, demonstrated: split below, new tab, split right, then quit. Ran Ctrl+p d → Ctrl+t n → Ctrl+p r → Ctrl+q .

A modern terminal workspace and multiplexer—the tmux alternative with sensible defaults, layouts, and a plugin system, so you do not spend a weekend writing a config file. Panes, tabs, floating windows and a session manager, with no config file to write first: keep a long agent run, a log tail and your shell in three panes without losing track of any of them. zellij setup --dump-config prints the full keybinding set.

🎨 pastel

https://github.com/sharkdp/pastel agent: yes, with --flag

pastel v0.12.0 — a hex colour shown as swatches and components. Ran pastel color on a hex value.

A command-line colour tool: generate, convert, mix and print colours in the format you need.

pastel format hex '#ff8800' # convert
pastel textcolor '#1e1e1e' --on white # WCAG contrast
pastel random | pastel format hex # the machine path

✅ Do

  • Generate a whole palette from one seed colour, so your theme comes out consistent by itself.
  • Use pastel format <type> whenever the output goes into a script or a variable. That is the documented pipeline.
  • Use pastel textcolor for contrast questions. Relative luminance with correct sRGB linearisation is easy to get wrong from memory, and nothing catches it.

🚫 Don't

  • Don't reach for -s . On pastel random it is --strategy and needs a value, so pastel random -s fails on argument parsing.
  • Don't parse the default output. Piped, it is HSL — not hex — because that is the human canvas format.
  • Don't use pastel random where the value has to be stable.
  • Don't use it to convert a single hex value to RGB. printf does that.

🤖 The Agent Playbook

Everything above is a tool. This section is written to be read by an agent — it addresses you directly, and it is the authoritative version of every rule repeated in a tool's section. Start here; treat the per-tool notes as the detail.

📌 Notes for agents

The substitution itself is one column: the Use instead column of the master install table . The reasoning, once and for all:

  • Plain text beats decoration. ls , cat and git --no-pager diff say what eza , bat and delta say, without icons, colour or a syntax gutter that are noise in a context window. bat and delta both pipe safely; the reasons to skip them are tab expansion and ANSI, not a pager that will hang you.
  • A pager with no way out is worse than no pager at all. Bare fzf and bare trip both wait for a keystroke. Use the non-interactive form or the substitute; never scrape a TUI to get a number.
  • One process beats an interactive navigator. jq replaces jnv outright — same goal, no TTY, no flags to remember.
  • Bound the output before it reaches context. Bare dust / and a full-depth walk will flood a window that dust -j -n 20 / fills with twenty useful lines.
  • A ranking is not an answer. zoxide scores directories by a clock-dependent formula and sorts ties unstably. If you know the path, use the path.
  • Establish ownership before you stop something. kill on an unknown PID is how a running database loses unsaved work. Run witr with a name first.
  • atuin has no substitute, and needs none. Never install it, never run it, never read its database.

No substitute at all — do not run them: yazi , lazygit , lazydocker , zellij , bottom , bandwhich , micro , edit , starship , zoxide .

⚖️ The rules that apply to every tool

💸 Output volume, in concrete terms

Two costs matter: the command you type, and the output that comes back.

The command you type:

Instead of Write chars
find . -name "*notes*" -type f fd notes 30 → 8
find . -type f -name "*.py" fd -e py 27 → 8
python -c "import json;print([i['id'] for i in json.load(open('f.json'))['items']])" jq -r '.items[].id' f.json 84 → 26

The output — this is what actually decides the context budget. Measured on the files behind this post ( cl100k_base tokenizer, whole source files, measured 2026-10-03):

What bytes, measured tokens, measured
SKILL.md — the whole agent guide, fetched at once 52,938 12,042
SKILL.yml — the selective-query contract 41,375 9,210
This post, English source 90,824 24,298
This post, Persian source 134,800 51,243

Two readings. First: fetching SKILL.md whole costs ≈12k tokens — that is exactly why SKILL.yml exists as a selective-query contract instead of a second full read. Second: Persian runs ≈1.8 characters per token against English ≈3.7 — the same content costs roughly twice the budget in Persian. Tokenizers change; remember the ratio, not the digits.

curl -s "$API/items" | python -m json.tool # every field, every item
curl -s "$API/items" | jq '.[] | {id, name, status}' # the four you needed

🤔 Should an agent search at all?

The cheat sheet below answers how do I search? , not should I search? :

Do you already know the file? -> address it directly, don't search
Do you know the string, not a path? -> rg
Do you know the path, not a string? -> fd
Is it "how big / what's in here"? -> dust, or --summary flags
Do you need the whole file? -> don't; slice it (jq, yq, rg -l)
Is it a TUI? -> stop. Say you can't see it, ask.

The last line matters most: when there is no non-interactive path to the answer, say so—do not improvise with a tool that can hang.

🗜️ rtk — the context compression layer

rtk v0.50.0 — RTK reporting how much context it has saved. Ran rtk gain .

A proxy that strips redundant content from command results before they reach the model's context. It does not replace the command; it filters that command's output.

git status # normal use: you do NOT type rtk
rtk git status # only when not being transparently rewritten

✅ Do

  • Keep writing normal commands. When the Hermes RTK integration is enabled, it transparently rewrites supported commands — don't prefix rtk manually on top of that, or you get double processing and a confusing trace.
  • Type rtk <command> explicitly only when the command is not being rewritten, when you need an RTK-specific function, or when debugging RTK itself.
  • Fall back to the raw underlying command whenever the summarized output isn't enough — RTK drops content on purpose, so it can never authorize a delete.

🚫 Don't

  • Don't read rtk gain as a token count. It divides output bytes by 4 and calls the result tokens, because rtk ships no tokenizer — upstream says the percentages are the meaningful number. There is no tokenizer benchmark behind it, so treat neither the counts nor the percentages as budget numbers.
  • Never treat summarized output as proof that something does not exist. If RTK omits it, that is not the same as it being absent.
  • Don't retry it over and over when it fails.

🧩 Tool selection cheat sheet

Paste this into your agent's system prompt.

If you need to… Use
Find files / directories fd
Search file contents or code rg
Fuzzy-filter a list with no TTY rg -F — fzf --filter is the slowest of the three on every sink, ~5x behind on a real pipe
Read a web page's article text defuddle parse -m
Read a page that only exists after JavaScript obscura fetch --dump markdown
Run your own JS across a batch of pages obscura scrape --eval
Parse or filter JSON jq
Parse or edit YAML / config yq
Inspect binary or protocol structure fq -V — for plain JSON use jq , which is 7.3x faster
Query or act on a GitHub repo, PR or issue gh
Test or replay HTTP workflows hurl
Diagnose network latency / loss ping -c 5 -q <host> — trip needs root
Manage a Python project, venv or tool uv
Lint and format Python ruff
Measure performance hyperfine
Inspect processes and resource usage procs --json
Diagnose process / port / service ownership witr
Find the largest directories on disk dust -j -n 20 /
Reduce noisy CLI output / context rtk
Jump to a directory you have been in before an absolute path — frecency ranking is not agent-safe
Convert a hex colour to RGB printf '%d %d %d' lpar;(0x1e)) lpar;(0x1e)) lpar;(0x1e))

🚫 What this post does not cover

Two deliberate gaps. Structural code search ( ast-grep ) is out of scope: rg answers "where is this string", not "where is this syntax pattern". Supply-chain inspection ( syft ) is out of scope too, even though every install here widens the attack surface — that is a scanner's job, not a shell tool's. Neither gap changes a recommendation above; reach for them when the question is the shape of code or the contents of an image.

On the test list. Everything above is an inspection or UI tool; nothing here checks correctness. That is the catalog's systematic bias, and the queue below starts fixing it.

Candidate Why it fits Status
ast-grep ( sg ) structural search: print($X) matches calls, not substrings; --json=stream satisfies the machine-readable rule next up — needs recording-host test + demo
mise Rust task runner and env manager in one binary; one tool where the post would otherwise need two queued
syft Go SBOM generator: answers "what is inside this image" queued

This post is maintained — the list shrinks as tools get tested, and a tool graduates only with a demo on the recording host.

🧾 Caveats

  • The atuin clip stops at the search results. Enter in that overlay runs the highlighted command, which is why the clip ends on the list instead of executing anything.
  • The hurl recording makes a real network request to httpbin.org , so its pass or fail depends on that service being reachable.
  • The gh recording shows a live stargazerCount — current on the recording date, stale the day after.
  • The obscura clip covers fetch and scrape only. serve and mcp are installed on the recording host, but neither is exercised on camera.
  • Every caption names the build that was on screen on the recording date, not the current release — see the recording toolchain . Several are behind, jq especially, where the newer line is mostly a security release.