Skip to content

Modern CLI Tools Footguns

Mistakes when using modern CLI replacements that trip up experienced engineers.


1. fd respecting .gitignore when you don't want it to

You run fd .env looking for .env files. It returns nothing. The files exist but they're in .gitignore. fd respects .gitignore by default, unlike find.

Fix: Use fd -H .env to include hidden files, or fd --no-ignore .env to ignore .gitignore rules. Know when defaults help and when they hide things.

Default trap: Both fd and rg respect .gitignore by default — the opposite of find and grep. This is usually helpful but dangerous when you're searching for files that are intentionally in .gitignore (like .env, node_modules/, build artifacts). Use --no-ignore when you need to find everything.


2. rg missing results in binary files

You search for a string with rg "pattern". It skips binary files by default. Your "binary file" is actually a UTF-16 encoded log file or a file with a binary preamble. Real matches are silently ignored.

Fix: Use rg --binary "pattern" to search binary files. Or rg -a "pattern" to treat all files as text. Be aware of the defaults.


3. jq silently returning null on typos

You write jq '.metdata.name' (typo: metdata instead of metadata). jq returns null without error. You think the field is empty. You build automation around a null value.

Fix: Use jq -e to exit with error on null/false. Use jq '.metadata.name // error("field not found")' for explicit error handling. Validate jq expressions against known-good data.

Gotcha: jq returns exit code 0 even when the output is null (unless you use -e). In shell scripts, result=$(echo '{}' | jq '.missing') sets result to the string null — not empty. Conditional checks like if [ -n "$result" ] pass because null is a non-empty string. Always use jq -e in scripts or check for the literal string null.


4. yq version confusion (Go vs Python)

There are two yq tools with different syntax. The Go version (mikefarah/yq): yq '.spec.replicas' file.yaml. The Python version (kislyuk/yq): yq -r '.spec.replicas' file.yaml. You copy a command from a blog post and it doesn't work because you have the other version.

Fix: Check which version: yq --version. Standardize on one across your team. The Go version (mikefarah/yq) is more common in DevOps contexts. Pin the version in your Dockerfile/CI.


5. fzf in scripts without --select-1 --exit-0

You use fzf in a script for interactive selection. When there's exactly one match, fzf still shows the interactive picker. When there are zero matches, fzf hangs waiting for input. Your automated script blocks.

Fix: Use fzf --select-1 --exit-0 to auto-select single matches and exit on no matches. Handle the exit code (1 = no match, 130 = interrupted).


6. Piping to xargs without -0 and filenames with spaces

You run fd -e yaml | xargs kubectl apply -f. A file named my config.yaml breaks into two arguments: my and config.yaml. kubectl can't find either.

Fix: Use null-delimited output: fd -e yaml -0 | xargs -0 kubectl apply -f. Or use fd -e yaml -x kubectl apply -f {} for per-file execution.

Under the hood: The null byte (\0) is the only character that cannot appear in a Unix filename. Newlines, spaces, tabs, quotes, and even glob characters are all legal in filenames. -0 / --null uses \0 as the delimiter instead of whitespace, making it safe for any filename. This is a POSIX concern, not a modern-CLI concern — find -print0 | xargs -0 is the classic equivalent.


7. tmux session surviving after logout

You start a long-running process in tmux and log out. The process keeps running, as intended. But you forget about it. Two months later, the process is still running, consuming resources, holding file locks, or writing to logs nobody reads.

Fix: Name your tmux sessions descriptively: tmux new -s migration-2024. List periodically: tmux ls. Kill finished sessions. Set up monitoring for long-running sessions.


8. bat as default pager breaking scripts

You alias cat to bat or set BAT_PAGER=less. A script that parses cat output now gets colored output with ANSI escape codes. Parsing breaks silently — fields don't match, regexes fail.

Fix: Use bat --plain (-p) when piping. Better: don't alias cat to bat. Use bat explicitly for reading, keep cat for piping. bat auto-detects pipes and disables formatting, but aliases can interfere.

Gotcha: bat detects whether stdout is a terminal (TTY) or a pipe and disables colors/formatting for pipes. But shell aliases bypass this detection in some contexts. If alias cat=bat and you run cat file | grep pattern, bat may still output ANSI codes depending on your shell and whether the alias expands before or after pipe detection. Use command cat to bypass aliases in scripts.


9. httpie caching cookies between requests

You test an API with http POST :8080/login. httpie stores the session cookie. Your next http GET :8080/admin silently uses the cookie and succeeds. You think the endpoint is unauthenticated, but it's using the cached session.

Fix: Use --session=test for intentional session persistence. Use http --print=hH to see full headers. Default httpie doesn't persist cookies between commands (sessions must be explicit), but be aware of this if using named sessions.


10. Over-relying on modern tools in environments that don't have them

You write all your scripts and runbooks using fd, rg, jq, and bat. You SSH into a production server that has none of these. You can't debug because you've forgotten how find, grep, and cat work.

Fix: Know the classic tools too. Your runbooks should use POSIX tools or document both options. Install modern tools in your base images, but don't depend on them for emergency debugging.

Remember: The equivalents to keep sharp: fd = find . -name, rg = grep -rn, bat = cat -n, sd = sed, dust = du -sh | sort -h, procs = ps aux. In an emergency on a minimal server, you'll only have POSIX tools and your muscle memory.