hy
Per-prompt shell history logger plus a hy <pattern> search shortcut. Sourceable files — one per shell — that share a common on-disk format.
What this does
Every command you run gets appended to a daily log file under ~/.logs/:
~/.logs/history-2026-04-25.log
~/.logs/history-2026-04-26.log
…
Each entry is exactly one line, tab-delimited:
TIMESTAMP ⇥ PWD ⇥ COMMAND
Two design choices fix the failure modes of the naive single-line history loggers people typically write:
- Tab-delimited fields, not space-delimited — paths with spaces (
/Users/me/Some Project) stay in their own field instead of bleeding into the command field. - Newlines and tabs inside the command are encoded as the two-char sequences
\nand\t. A multi-linefor … doneblock becomes one log entry instead of three orphan lines.
You then search across all daily logs with hy (oldest matches first) or hyr (newest first):
hy "rsync" # all commands containing "rsync"
hy -i FOR # case-insensitive
hy "dir with spaces" # works because tabs separate fields
hyr git # most recent git invocations first
The files
hy.bash
Hooks PROMPT_COMMAND idempotently. Uses printf '%(…)T' on bash 4.2+, falls back to date on stock /bin/bash 3.2.
hy.fish
Hooks fish_postexec — receives the command directly as $argv[1], so multi-line capture is automatic.
hy.ps1
Wraps PowerShell's prompt function idempotently. Uses Get-History for logging and Select-String for search.
Tip: file:// in some browsers ignores the download attribute and opens the file inline instead. If that happens, right-click the link and choose Save Link As…, or serve this folder over HTTP — e.g. python3 -m http.server from the hy/ directory, then open localhost:8000.
All implementations write the same on-disk format, so you can switch shells and hy still searches across history written by any of them.
Installation
Each one-liner downloads the right file to ~/.config/hy/ (or the conventional fish location) and wires it up. Use the Copy button, paste into your shell, hit return.
zsh
Downloads to ~/.config/hy/hy.zsh and adds a guarded source line to ~/.zshrc:
mkdir -p ~/.config/hy && curl -fsSL "__BASE__/hy.zsh?v=$(date +%s)-$$" -o ~/.config/hy/hy.zsh && { grep -qxF 'source ~/.config/hy/hy.zsh' ~/.zshrc 2>/dev/null || echo 'source ~/.config/hy/hy.zsh' >> ~/.zshrc; } && source ~/.config/hy/hy.zsh
Registered via add-zsh-hook precmd — coexists with oh-my-zsh, starship, prezto, or your own existing precmd. Re-sourcing the file won't duplicate the hook.
bash
Downloads to ~/.config/hy/hy.bash and adds a guarded source line to ~/.bashrc:
mkdir -p ~/.config/hy && curl -fsSL "__BASE__/hy.bash?v=$(date +%s)-$$" -o ~/.config/hy/hy.bash && { grep -qxF 'source ~/.config/hy/hy.bash' ~/.bashrc 2>/dev/null || echo 'source ~/.config/hy/hy.bash' >> ~/.bashrc; } && source ~/.config/hy/hy.bash
fish
Drops the file straight into ~/.config/fish/conf.d/ — fish auto-loads anything in there, no extra source line needed:
mkdir -p ~/.config/fish/conf.d && curl -fsSL "__BASE__/hy.fish?v="(date +%s)"-$fish_pid" -o ~/.config/fish/conf.d/hy.fish && exec fish
PowerShell
Downloads to $HOME/.config/hy/hy.ps1 and adds a guarded dot-source line to $PROFILE.CurrentUserAllHosts:
$HyDir = Join-Path $HOME ".config/hy"; New-Item -ItemType Directory -Force $HyDir | Out-Null; $HyFile = Join-Path $HyDir "hy.ps1"; Invoke-WebRequest -UseBasicParsing "__BASE__/hy.ps1?v=$([DateTime]::UtcNow.Ticks)-$PID" -OutFile $HyFile; if (Get-Command Unblock-File -ErrorAction SilentlyContinue) { Unblock-File -Path $HyFile }; $HyProfile = $PROFILE.CurrentUserAllHosts; New-Item -ItemType Directory -Force (Split-Path -Parent $HyProfile) | Out-Null; $HySource = '. "$HOME/.config/hy/hy.ps1"'; if (-not (Test-Path $HyProfile) -or -not (Select-String -Path $HyProfile -SimpleMatch $HySource -Quiet)) { Add-Content -Path $HyProfile -Value $HySource }; . $HyFile
What gets defined
| Name | Kind | Purpose |
|---|---|---|
precmd (zsh) / __hy_log (bash, fish, PowerShell) | hook | Runs before/after each prompt; appends one log entry. |
hy <pattern> [options…] | function | Searches across ~/.logs/history-*.log. Chronological order. |
hyr <pattern> [options…] | function | Same as hy, newest match first. |
Verification
After sourcing, try each failure mode and confirm hy finds it:
mkdir -p "/tmp/dir with spaces" && cd "/tmp/dir with spaces" && ls
# zsh/bash multi-line:
for i in 1 2 3; do
echo $i
done
# plain:
git status
tail ~/.logs/history-$(date +%Y-%m-%d).log
# Every line should have exactly two tab characters.
# The for-loop appears as ONE line containing literal `\n` between statements.
# The space-containing path appears intact in field 2.
hy ls
hyr ls
hy -i FOR
hy "dir with spaces"
Notes & trade-offs
- Decoding for display: if you want the original layout of a multi-line command back, pipe a log line through
printf '%b\n'orsed 's/\\n/\n/g; s/\\t/\t/g'. - Old log entries (from any prior, space-delimited logger) remain in their existing files.
hystill finds them by content — grep doesn't care about field structure — but they aren't tab-parseable. - Search options: shell implementations use
grepoptions. PowerShell usesSelect-Stringand supports-i/--ignore-caseplus-F/--fixed-strings. - Root/elevated sessions are skipped so a sudo'd or administrator shell doesn't pollute your personal history.
- First prompt of a fresh session may log the previous shell's last command (zsh/bash) — same edge case as the original simple logger, accepted in exchange for no per-command bookkeeping. PowerShell initializes its last seen history id when sourced to avoid that.
- PowerShell prompt customizers loaded after
hy.ps1can replace the wrapped prompt and stop logging. Put thehy.ps1dot-source line after prompt customizers in your profile. ~/.logsis created on demand if missing, so deleting the folder doesn't break the next prompt.