DsgnWrks

The random technical musings of Justin Sternberg

Steal This Tool: dirmap + goto

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:

  1. dirmap — a CLI that maintains a JSON file of alias => directory path bookmarks and prints paths/keys to stdout. Any language works; it’s just JSON CRUD + stdout.
  2. goto — a tiny shell function (this part must be shell — a child process can’t cd your interactive shell) that calls dirmap <key> and cds 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 $HOME when 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 bare goto takes 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).

CommandBehavior
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 listPretty table: aliases sorted, colorized, padded, paths shown ~-relative.
dirmap list --keys / -kOne alias per line — this feeds tab completion.
dirmap list --json / -jRaw JSON, pipe-friendly.
dirmap commandsOne subcommand name per line (add, remove, rename, identify, list, get, …). The goto function uses this to avoid treating subcommand names as aliases.
dirmap help / --helpUsage 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)

  1. Storage layer — load/save ~/.dirmap.json; create with {} if missing; ~ expansion helpers both directions.
  2. Core commandsget, add, remove, list (+ --keys, --json). At this point goto already works.
  3. Shell gluegoto function + completion for your shell(s). Wire dirmap commands output.
  4. Nice-to-havesrename, identify (needs the realpath canonicalization above), pretty colorized list.
  5. Polish — helpful errors (“no such key, try dirmap list”), --help docs, 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:

Acceptance checklist

  • dirmap add in a directory, cd /, goto <alias> lands you back there
  • goto [TAB] completes your aliases
  • ☐ Bare goto goes $HOME
  • goto badkey prints a helpful error and does not cd
  • dirmap list -k output is clean (one key per line, no colors)
  • dirmap identify works 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.

Leave a Reply

Your email address will not be published. Required fields are marked *