Shell Productivity: tmux, fzf & direnv
Keep long go test runs alive in tmux, jump to packages with fzf, and load per-module Go settings with direnv in monorepos.
Summary
Go monorepos spend more time in the terminal than many web stacks: module roots, multiple go.mod trees, and CI-parity test commands.
tmux, fzf, and direnv reduce friction so you spend attention on failures, not on lost sessions or wrong GOPRIVATE values.
Recipe
Quick-reference recipe card - copy-paste ready.
# direnv: auto-load env when entering a module
echo 'export GOTOOLCHAIN=auto' >> .envrc
echo 'export GOPRIVATE=github.com/myorg/*' >> .envrc
direnv allow
# fzf: fuzzy-find a package directory
fd -t d . internal | fzf --preview 'ls {}'
# fzf + ripgrep: jump to symbol
rg -l "func HandleHealth" --glob '*.go' | fzf -m | xargs -r ${EDITOR:-vim}
# tmux: named session for long test run
tmux new -s gotest
go test ./... -race -count=1 2>&1 | tee test.log
# detach: Ctrl-b d ; reattach: tmux attach -t gotestWhen to reach for this:
- SSH drops kill a 20-minute
go test ./...run. - You switch between several
go.modroots daily. - You want ripgrep results with interactive preview before opening an editor.
Working Example
#!/usr/bin/env bash
# ~/.config/dotfiles/go-monorepo-setup.sh (illustrative)
# 1) direnv in a service module
cd ~/src/myorg/platform/services/api
cat > .envrc <<'EOF'
export GOTOOLCHAIN=auto
export GOPRIVATE=github.com/myorg/*
export PATH="$(go env GOPATH)/bin:$PATH"
layout go
EOF
direnv allow
# 2) fzf keybinding: open file from repo (requires fzf installed)
export FZF_DEFAULT_COMMAND='rg --files --hidden --glob "!vendor/**" --glob "!*.git/*"'
open_go_file() {
local file
file=$(fzf -m --preview 'bat -n --color=always {} 2>/dev/null || head -100 {}') || return
${EDITOR:-vim} $file
}
# 3) tmux session with windows for watch + tests
tmux new-session -d -s api -n shell
tmux send-keys -t api:shell "cd ~/src/myorg/platform/services/api && direnv allow ." C-m
tmux new-window -t api -n test
tmux send-keys -t api:test "go test ./... -count=1 -race 2>&1 | tee ~/artifacts/api-test.log" C-m
tmux attach -t apiWhat this demonstrates:
.envrcwithGOPRIVATEandlayout gofor module-aware paths.- fzf fed by ripgrep file lists with preview.
- tmux windows separating shell env setup from long-running tests.
Deep Dive
How It Works
- tmux is a terminal multiplexer: sessions survive SSH disconnects; windows and panes split logs, tests, and editors.
- fzf is a fuzzy finder over any list (files, git branches,
go testpackage paths) with optional preview commands. - direnv hooks shell
cdand exports variables from.envrcafterdirenv allow, preventing leakedGOPRIVATEacross unrelated projects. - Together they standardize monorepo navigation without a heavyweight IDE for every remote host.
Tool Roles
| Tool | Solves | Typical Go command |
|---|---|---|
| tmux | Session persistence | go test ./... overnight |
| fzf | Fuzzy selection | pick internal/billing package |
| direnv | Per-directory env | GOTOOLCHAIN, GOPRIVATE |
| ripgrep + fzf | Code navigation | jump to handler definition |
fd | Fast file lists | feed fzf with directories |
direnv Snippets for Go
# .envrc
use go # if available in direnv stdlib
export GOTOOLCHAIN=auto
export GOPRIVATE=*.corp.example.com,github.com/myorg/*
export GOLANGCI_LINT_CACHE=$PWD/.cache/golangci-lint
PATH_add "$(go env GOPATH)/bin"Run direnv allow once per directory after editing .envrc.
Go Notes
# fzf: pick a package and test it
go list ./... | fzf -m | xargs -r go test -count=1Combine with go list -json for module path metadata in advanced previews.
Gotchas
- Forgotten
direnv allow-.envrcchanges silently do nothing. Fix: aliasda='direnv allow'and hook shell prompt to show direnv status. - tmux scrollback vs mouse - Copy/paste differs from terminal defaults. Fix:
set -g mouse onin~/.tmux.confand learn copy mode (Ctrl-b [). - fzf without
.gitignorerespect - Feeding plainfindincludes vendor noise. Fix:rg --filesorfdwith ignore rules. - GOPRIVATE leakage - Exporting in
.bashrcapplies org-wide to personal projects. Fix: direnv per repo only. - Multiple tmux sessions same name - Attach fails confusingly. Fix:
tmux list-sessionsand name by service (api,worker). - Slow fzf preview on huge files - Previewing multi-MB generated files lags. Fix: preview
headorbat --line-range 1:80.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| tmux | SSH remote dev | Local IDE integrated terminal only |
| GNU screen | tmux unavailable | Greenfield - prefer tmux features |
| IDE workspace per module | Rich refactor tools | Minimal remote VM |
just / Make tasks | Repeatable team commands | Exploratory fuzzy navigation |
| Zellij | Modern multiplexer UX | Team already standardized on tmux |
FAQs
Do I need tmux if I use VS Code Remote SSH?
Remote SSH recovers editor state but long purely-shell test runs still die if the remote connection drops without tmux.
How does layout go in direnv help?
It sets GOPATH layout conventions for legacy GOPATH workflows.
Module mode projects mostly need PATH_add and env exports, not full GOPATH src layout.
Can fzf replace cd?
fzf-cd-widget (zsh) or custom bash bindings jump directories quickly.
Pair with zoxide for frequent paths.
What tmux prefix should I use?
Default Ctrl-b is fine.
Some maps prefix to Ctrl-a for Emacs users - document team config in dotfiles repo.
How do I share tmux with another developer?
tmux sockets and SSH multiattach are possible but rare.
Prefer shared logs in artifacts over shared interactive sessions.
Does direnv work with fish shell?
Yes with direnv's fish hook.
Go env vars apply the same once allowed.
How do I fuzzy-pick git branches?
git branch | fzf | xargs git checkout is a common binding.
Useful when release branches multiply.
Should .envrc live in git?
Yes for non-secret defaults.
Never commit secrets - reference 1Password or vault paths instead.
Can I run golangci-lint in a tmux pane watch mode?
Yes.
watchexec or entr on *.go pairs well with a lint pane beside go test watch.
What packages install these tools on Ubuntu?
apt install tmux fzf direnv ripgrep fd-find (binary may be fdfind).
macOS: Homebrew equivalents.
How do I avoid duplicate go test in two panes?
One pane owns the long ./... run; others run scoped go test ./internal/pkg -run TestName.
Is fzf required for monorepos?
No, but it pays off when go list ./... returns hundreds of packages.
Related
- Linux CLI Basics - foundational shell commands
- Go Toolchain on Linux: Install & Version Switching - GOTOOLCHAIN in direnv
- Search, ripgrep & jq for Debugging - ripgrep pipelines into fzf
- Build, Test & Profile from the Shell - long test runs
- Packages and Modules Basics - GOPRIVATE and modules
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).