Tty-aware shell wrappers for OSC 8 hyperlinks in eza, ripgrep, and fd
Shell functions that add OSC 8 hyperlink flags to eza, ripgrep, and fd only when stdout is a terminal, so clickable paths never leak into pipes or files.
I wrap eza, ripgrep, and fd in shell functions that emit OSC 8 hyperlinks only when their output goes to a terminal, so clickable paths never leak into pipes or files.
The Problem
OSC 8 hyperlinks turn each path into a clickable link (cmd-click in kitty opens the file). The mechanism is raw escape bytes wrapped around the visible text:
\e]8;;file:///abs/path\e\\filename\e]8;;\e\\
A static alias like alias rg='rg --hyperlink-format=default' injects those bytes unconditionally. That breaks everything downstream: rg foo | wc -l counts the escape bytes, rg foo > matches.txt writes garbage, and any tool parsing the output sees the escape sequence instead of a path.
What I want is hyperlinks when a human reads a terminal, and plain text everywhere else.
What Changed
tty-aware shell-function wrappers in ~/.commonrc, each guarded by _have so they are only defined where the tool exists:
# ~/.commonrc
# _have is defined once near the top of the file
_have() { type "$1" &>/dev/null; }
# ------------------------- conditional tools ------------------------
# Terminal hyperlinks (OSC 8) for kitty plain-click on paths
if _have eza; then
unalias ls 2>/dev/null
ls() {
if [[ -t 1 ]]; then command eza --hyperlink=auto "$@"
else command eza "$@"; fi
}
alias lt='eza --tree --level=2 --long --icons --git --hyperlink=auto'
fi
if _have rg; then
rg() {
if [[ -t 1 ]]; then command rg --hyperlink-format=default "$@"
else command rg "$@"; fi
}
fi
if _have fd; then
fd() {
if [[ -t 1 ]]; then command fd --hyperlink=auto "$@"
else command fd "$@"; fi
}
fi
Why each piece is the way it is:
[[ -t 1 ]]is true only when file descriptor 1 (stdout) is a real terminal.ls > out.txt,rg foo | wc -l, andfd . | xargs rmall take theelsebranch and get clean output.command ezacalls the real binary instead of the function. Without it,ls()would call itself forever.- A function, not an alias. Aliases are static text substitution; they cannot make a runtime decision based on whether stdout is a tty, so the branch needs a function. For eza I also
unalias lsfirst, because a plainlsalias is defined earlier in the same config and would otherwise shadow the function. - The
_haveguard means one.commonrcports across machines that may lack eza, rg, or fd without printing errors.
The flag differs per tool: eza and fd take --hyperlink=auto, ripgrep takes --hyperlink-format=default. eza and fd’s flags are clap optional-value options, so the value must be attached with =. A bare --hyperlink swallows the next argument. I wrote about that failure mode separately.
Results
Direct invocation produces clickable paths; piping or redirecting the same command produces clean output. The difference is visible with cat -v. Unwrapped eza piped into a file dumps the raw OSC 8 sequence:
$ command eza --hyperlink=always src/content/blog | head -1 | cat -v
^[]8;;file:///Users/fran/.../a-cognito-...-powersync.md^[\a-cognito-...-powersync.md^[]8;;^[\
The wrapped ls, piped through the same chain, is clean:
$ ls src/content/blog | head -1 | cat -v
a-cognito-token-pair-contract-api-gateway-authorizer-spring-hono-and-powersync.md
And rg, piped into anything, behaves like the bare binary:
$ rg -n "draft: true" src/content/blog/eza-hyperlink-eats-the-next-positional-argument.md | head -1
5:draft: true
Run interactively (stdout is a tty), the same commands emit OSC 8 links that kitty cmd-click opens.
There is one more line that hands the hyperlink job to kitty’s own ripgrep kitten:
_have kitten && hg() { kitten hyperlinked_grep "$@"; }
What I’d Do Differently
The three wrappers look repetitive, and I considered generating them from a table of {command, flag} pairs. The per-tool flag names differ (--hyperlink vs --hyperlink-format), and the eza wrapper needs the unalias ls step, so a generator would hide more than it saves. Three readable functions win.
References
- Hyperlinks in Terminal Emulators (OSC 8 spec): the escape sequence format all three tools emit.
- kitty: hyperlinked_grep kitten: kitty’s built-in ripgrep-with-hyperlinks, what the
hghelper calls. - eza(1)
--hyperlink=WHEN: the flag thelswrapper adds. - fd(1)
--hyperlink: sameauto/always/nevershape as eza. - ripgrep:
--hyperlink-format, documented inrg --help.
This post was written with AI assistance.