🗺 Map
🗺 Map
🔎 Find & Read Does this file exist, and what's in it?
🧬 Inspect & Query Turn data into answers.
✏️ Edit & Review Edit files, review what changed.
📊 System & Network What is actually happening on this machine?
⚙️ Build & Verify Environments, linting, benchmarks.
🗺️ Navigate & Multiply Move faster, remember more, multiply sessions.
🩺 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 themjq '.logging' config.json # the answer, and nothing elseThat 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:
- The core.
fd,ripgrep,jq,yq,hyperfine. No TUI, no root, no config. Copy the one-liner and stop. - The agent layer.
rtkandprocs. Add the rest when a real task asks. - 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
--helpand 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 bucketscoop 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 obscuraTwo 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/vcredist2022Scoop 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 defuddleOr 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 ripgrepwinget install --id BurntSushi.ripgrep.MSVC -e --silent ` --accept-package-agreements --accept-source-agreements --disable-interactivityNeither 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.comThe 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" --versionwhere.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 herefd --glob '*.py' # or switch to glob matchingfd -e py # only .py filesfd --type d --max-depth 3 src # bound it: three levels, directories onlyfd -0 # NUL-separated, so paths with spaces survive✅ Do
- Use
fdto find a path ; usergto find a string . Confusing the two is the most common agent misuse. - Always narrow with
--type,--extension,--max-depthor--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>beatscdthen a barefd.
🚫 Don't
- Don't use
rg --filesfor filenames whenfddoes 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 treerg -l "os.environ" # filenames only: tiny outputrg -c "TODO" # counts per file: cheapest survey✅ Do
- Search narrow, then widen.
rg -g '*.ts' "token" src/beforerg "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__—.gitignoredoesn't always cover generated directories. - Don't assume a missing hit means a missing thing.
rgdoes 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 statuseza --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 numbersbat --diff # only the lines that changed🎯 fzf
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-interactivefd --type f | fzf --preview 'bat --color=never {}' # the human picker✅ Do
- Pair the interactive form with
fd(FZF_DEFAULT_COMMAND) andbat(--preview) for a good file opener.
🚫 Don't
- Don't leave
find . undefined fzfin 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 directoryyazi ~/Downloads # or start somewhere elsePreviews are the headline. Two external helpers make it great:
sudo apt install chafa # render images inlinesudo apt install glow # terminal renderer for .mdmkdir -p ~/.config/yazi # config dir, created if absentcat > ~/.config/yazi/yazi.toml << 'EOF' # [plugin]: prepend a preview rule[plugin] # ...matched on mimeprepend_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 -mskips the temp file entirely. - Use
-pto 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
obscurais 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 markdownfor the text,--dump linksfor theurl<TAB>titlegraph,--dump assetsfor the sub-resource list as NDJSON. obscura scrape url1 url2 …for the same across a batch, in parallel. Its JSON carriestotal_urls,concurrency, a worker id per result and per-URLtime_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-networkfor anything you didn't start yourself. It disables that SSRF protection, which on a shared network is a genuine attack surface. - Put
obscura serveorobscura mcpon 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 firstjq -r '.items[].id' results.json # -r = raw strings, not quoted JSON✅ Do
- Pipe large JSON through
jqbefore 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 returnsnulland the agent will build on that. - Don't mutate important JSON blindly. Write to a new file, verify, then move it.
- Don't use
jqon HTML or a log line. That'srg.
📄 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 outyq -o=json '.services' compose.yml # as JSON, for a programyq -i '.services.api.replicas = 3' compose.yml # structural edit, never sed✅ Do
- Prefer
yq -ioversedfor YAML every time . Asedthat 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=jsonwhen a program reads the result.-Iis--indent, not a document separator, so it is not the flag for one-document-per-line. - Keep in mind that yq has a
systemoperator, and that it is disabled by default — the real form issystem("<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-opsand--security-disable-file-opsdisable the related built-ins.
🚫 Don't
- Don't
apt install yqand assume you got this one. On Debian/Ubuntuyqis a completely different Python tool with jq syntax. Confirm withyq --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 namedcat /etc/hostname.system("hostname"),system("cat")andsystem("id")do run. - Don't rewrite a whole YAML file to change one field. Diff the result.
🔬 fq
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 dumpfq -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-Mand-o array_truncate=0 -o string_truncate=0for 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.jsoncat 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/getHTTP 200[Asserts]jsonpath "$.url" == "https://httpbin.org/get"hurl --test test.hurl✅ Do
- Use it instead of regenerating one-off
curlcommands 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>— sotoken: jsonpath "$['access_token']", referenced later as{{token}}. A request header is a plainName: valueline in the entry, notheader "..."; 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 -1is 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 logingh repo view charmbracelet/vhs --json name,description,stargazerCount,url✅ Do
- On commands that support it, pass
--jsonwith 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_TOKENwhile recording.
🚫 Don't
- Don't use
ghas a general HTTP client.gh api --hostnamedoes reach GitHub Enterprise andghe.comtenancy hosts, but it only speaks the GitHub API shape, so it won't fetch an arbitrary non-GitHub API the waycurlwould. - Don't quote a live value like
stargazerCountas 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.txtexport 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 filenamesudo 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.pagergit --no-pager diff | delta --paging=never --color-onlySet 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 20first — bounded output, and it answers "what's big here?" in a single run. Add-jwhen a program reads the result, and keep-ntoo: the row limit bounds the output. - Use
-dto control depth. A full-depthdust /is a context flood, so don't run one on a machine you don't own.
🚫 Don't
- Don't reach for
-jto make a scan parallel. Threading is-T/--threads; indust,-jis--output-json. And don't read a ratio againstduas a property of the tool:dust's speed comes from threads, so it beatsduonly when cores are free. Pinned to one thread (-T1) it is slower thandu, and on an 8-vCPU native hostduwas 2.3x faster. Usedustfor-nbounded output and-jJSON, not for speed. - Don't loop
du -sh. That's whatdustreplaced.
🌳 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 processprocs --tree # the first move when something misbehaves✅ Do
- Use
procs --jsonwhen 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--jsonis missing, check the CHANGELOG. - Use
procs --treeas the first move when something behaves strangely. The parent/child shape usually explains the problem on its own. - Work up deliberately:
procsfor a first look, then/proc,pswith explicit columns,ss,systemctlordocker inspectwhen you need the real answer. - Reach for
witrwhen 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.
python3might be anything. - Don't ask for the
Envcolumn. It prints another process's full environment, which is where API keys live, and that output lands in your context. - Don't combine
--jsonwith--onlyor--treeon v0.14.12 — a skipped column produces invalid JSON. Check that the output parses. - Don't replace
systemctl statusordocker inspectwithprocswhen 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
witrbefore 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 -9a 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
btmto get a number. It is a full-screen TUI that waits for a keystroke, and there is no report mode: readprocs,free -handss -sinstead, ordocker statsfor 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 capabilitiessudo 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 TUItrip 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 venvuv add requestsuv run python main.py # resolves the right env every timeuvx 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
uvxfor one-shot tools — no install, no pollution. - Keep
uv.lockauthoritative for the team viauv add/uv sync.
🚫 Don't
- Don't mix
pip installanduvin the same environment. You'll get dependency state neither tool can reason about. - Don't
uv pip installinto 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 gateruff check --output-format=json . # for a program, not a person✅ Do
- Run
ruff checkafter 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
ruffconfig — 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-fixescasually — "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
--warmupso 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 baretimecannot. 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-runsdefaults to 10, and--prepareruns before each timing run, so agit commitorcurl -X POSTruns 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.
🗺️ Category 6 — Navigate & Multiply
🔖 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 ~/.bashrcz 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_xxxxxxxxxxxxcurl -H "Authorization: Bearer $API_KEY" https://api.example.com/...psql postgres://admin:hunter2@db.internal/appmysql -uroot -p'correct horse battery staple' < dump.sqlAWS_SECRET_ACCESS_KEY=wJalr... git push https://user:token@github.com/org/repo.gitRun 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 registerTwo 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
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' # convertpastel textcolor '#1e1e1e' --on white # WCAG contrastpastel 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 textcolorfor 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. Onpastel randomit is--strategyand needs a value, sopastel random -sfails 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 randomwhere the value has to be stable. - Don't use it to convert a single hex value to RGB.
printfdoes 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,catandgit --no-pager diffsay whateza,batanddeltasay, without icons, colour or a syntax gutter that are noise in a context window.batanddeltaboth 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
fzfand baretripboth 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.
jqreplacesjnvoutright — 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 thatdust -j -n 20 /fills with twenty useful lines. - A ranking is not an answer.
zoxidescores directories by a clock-dependent formula and sorts ties unstably. If you know the path, use the path. - Establish ownership before you stop something.
killon an unknown PID is how a running database loses unsaved work. Runwitrwith a name first. atuinhas 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 itemcurl -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 searchDo you know the string, not a path? -> rgDo you know the path, not a string? -> fdIs it "how big / what's in here"? -> dust, or --summary flagsDo 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 rtkrtk 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
rtkmanually 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 gainas 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
atuinclip stops at the search results.Enterin that overlay runs the highlighted command, which is why the clip ends on the list instead of executing anything. - The
hurlrecording makes a real network request tohttpbin.org, so its pass or fail depends on that service being reachable. - The
ghrecording shows a livestargazerCount— current on the recording date, stale the day after. - The
obscuraclip coversfetchandscrapeonly.serveandmcpare 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.
