The why
If you work in a terminal all day, you spend a dumb amount of it typing paths. The projects you touch constantly live in places like ~/Sites/clients/acme/wp-content/plugins/acme-forms — and you re-type (or fuzzy-history-search) that walk dozens of times a week. Directory bookmarks fix this: you name a directory once, then jump to it from anywhere with a short alias:
goto acme # from anywhere, you're in the plugin dir
“But z/autojump/zoxide already do this!” Sort of — those are frecency tools: they guess where you want to go based on your history, and the guess changes over time. dirmap/goto is deliberate: an alias means exactly one directory, forever, because you said so. goto acme never surprises you. The two approaches coexist fine; this one is the “muscle memory you can trust” half. And because the map is a plain JSON file with a scriptable CLI around it, it becomes infrastructure: other scripts, aliases, and AI agents can resolve dirmap get acme to a real path too.
I’ve been daily-driving this for ~10 years as a PHP script in my dotfiles. This post is the spec so you (or more likely, your coding agent) can rebuild it in whatever language you like in about an hour, without needing PHP or my dotfiles setup.
A concrete example of the “infrastructure” claim: the Hotline Claude Code plugin — cross-workspace communication for Claude agents — uses ~/.dirmap.json as its phone book. Each dirmap alias doubles as a workspace “phone number,” so “call the blog workspace” resolves through dirmap get blog. Build a compatible dirmap and you get a working agent phone directory for free.
Reference implementation (PHP): bin/dirmap
What it is
Two pieces that work together:
dirmap— a CLI that maintains a JSON file ofalias => directory pathbookmarks and prints paths/keys to stdout. Any language works; it’s just JSON CRUD + stdout.goto— a tiny shell function (this part must be shell — a child process can’tcdyour interactive shell) that callsdirmap <key>andcds to the result, plus tab completion for the aliases.
Day-to-day usage:
cd ~/Sites/clientproject dirmap add # bookmarks cwd under alias "clientproject" dirmap add cp # or bookmark cwd under alias "cp" cd ~ goto cp # jump back — with tab completion: goto [TAB] dirmap list # pretty-print all bookmarks
The data file
~/.dirmap.json — a flat JSON object:
{"cp": "/Users/you/Sites/clientproject", "dots": "~/.dotfiles"}
- Values may contain
~; expand to$HOMEwhen reading, and prefer storing~-relative paths for portability/readability. - Create the file with
{}on first run if missing. - The empty key
""is implicitly~/at runtime (so baregototakes you home) but is never written to the file.
CLI spec
All “output” is bare stdout (no decoration) unless noted — the outputs are consumed by shell functions and completions, so keep them script-friendly. Exit 0 on success, 1 on errors (with the error message on stderr).
| Command | Behavior |
|---|---|
dirmap <key> / dirmap get <key> | Print the expanded absolute path for <key>. Unknown key → error + hint to run dirmap list, exit 1. Empty key → print $HOME. |
dirmap add [<key>] [<path>] | Add/overwrite a mapping. <path> defaults to cwd (also when given as .); <key> defaults to the basename of cwd. Confirm with a friendly message. |
dirmap remove <key> | Delete a mapping. Key required. |
dirmap rename [<oldkey>] <newkey> | Rename an alias. With one arg, treat it as the new key and find the old key by matching cwd against stored paths (see identify). Errors: unknown oldkey, newkey already exists, oldkey == newkey. |
dirmap identify [<path>] | Reverse lookup: print the alias for a path (default cwd). If several aliases point at the same path, print the shortest. No match → exit 1. |
dirmap list | Pretty table: aliases sorted, colorized, padded, paths shown ~-relative. |
dirmap list --keys / -k | One alias per line — this feeds tab completion. |
dirmap list --json / -j | Raw JSON, pipe-friendly. |
dirmap commands | One subcommand name per line (add, remove, rename, identify, list, get, …). The goto function uses this to avoid treating subcommand names as aliases. |
dirmap help / --help | Usage docs. |
Path-comparison gotcha (identify/rename)
When matching a path against stored paths, canonicalize both sides (realpath / fs.realpathSync / os.path.realpath): expand ~, strip trailing /, and resolve symlinks (macOS /tmp vs /private/tmp, or a symlinked ~/Sites) so paths compare by their physical location. Fall back to the string path if it doesn’t exist on disk.
The shell glue
Distribute alongside the binary (zsh version shown; adapt for bash/fish):
goto() {
# don't cd if the arg is actually a subcommand (e.g. `goto list`)
if dirmap commands | grep -qx -- "$1"; then
dirmap "$@"
return
fi
local output
output=$(dirmap "$1")
if [ $? -eq 0 ]; then
cd "$output"
else
echo "$output"
fi
}
_goto() {
local mapkeys=$(dirmap list -k)
_arguments -C "1: :(--help $mapkeys)"
}
compdef _goto goto
_dirmap() {
local mapkeys=$(dirmap list -k)
_arguments -C "1: :(--help $mapkeys)"
}
compdef _dirmap dirmap
Completion pulls live keys via dirmap list -k every time — no caching, no stale completions.
Implementation plan (any language)
- Storage layer — load/save
~/.dirmap.json; create with{}if missing;~expansion helpers both directions. - Core commands —
get,add,remove,list(+--keys,--json). At this pointgotoalready works. - Shell glue —
gotofunction + completion for your shell(s). Wiredirmap commandsoutput. - Nice-to-haves —
rename,identify(needs the realpath canonicalization above), pretty colorizedlist. - Polish — helpful errors (“no such key, try
dirmap list”),--helpdocs, exit codes.
An agent can build steps 1–3 in one sitting; the spec above plus the reference source is everything it needs. Point it at the reference for behavior questions:
- CLI: bin/dirmap
- goto plugin: goto-dirmap.plugin.zsh
- completions: dirmap-completions.plugin.zsh
Acceptance checklist
- ☐
dirmap addin a directory,cd /,goto <alias>lands you back there - ☐
goto [TAB]completes your aliases - ☐ Bare
gotogoes$HOME - ☐
goto badkeyprints a helpful error and does notcd - ☐
dirmap list -koutput is clean (one key per line, no colors) - ☐
dirmap identifyworks from a symlinked path - ☐ JSON file survives round-trips (keys with spaces, paths with spaces, no clobbering)
Spec derived from the live PHP implementation in jtsternberg/Dot-Files. Reimplement in whatever language your agent likes.