Tiny bash CLI that replaces cd /long/path/to/project with cdp <label>. Per-project macros (defined in an SSH-style config file) let you bundle a jump with the commands you usually type after it: cdp myproject deploy jumps and runs the deploy steps.
# Jump to a project
cdp myproject
# Jump and run a macro defined for that project
cdp myproject deploy
# Add / list / remove projects
cdp add myproject /home/user/myproject
cdp ls
cdp rm myprojectNo npm, no composer, no Go binary, no Python. Bash 4+, coreutils, and an flock from util-linux are all you need at runtime. tmux is required only if you use Tmux blocks; macros and bare jumps work without it.
git clone https://github.com/mihai-valentin/cdp.git
cd cdp
make install # installs to $HOME/.local
# Add this to your ~/.bashrc or ~/.zshrc:
eval "$(~/.local/bin/cdp init bash)"make install puts cdp in $PREFIX/bin, scripts in $PREFIX/libexec/cdp/, and helper libs in $PREFIX/lib/cdp/. Override the prefix with make install PREFIX=/usr/local.
./install.shinstall.sh probes your PATH and picks $HOME/.local or /usr/local accordingly. Override with ./install.sh --prefix=<path>.
VERSION=1.11.0
curl -sLO https://github.com/mihai-valentin/cdp/releases/download/v${VERSION}/cdp-${VERSION}.tar.gz
curl -sLO https://github.com/mihai-valentin/cdp/releases/download/v${VERSION}/cdp-${VERSION}.tar.gz.sha256
sha256sum -c cdp-${VERSION}.tar.gz.sha256
tar -xzf cdp-${VERSION}.tar.gz
cd cdp-${VERSION} && ./install.shmake uninstallcdp <label>cdp myproject resolves the myproject project's Path and cds your shell into it. The shim function emitted by cdp init performs the actual cd — a regular binary cannot mutate its parent's cwd.
cdp <label> <macro>cdp myproject deploy jumps to myproject and then runs the deploy macro's Run lines, in order, in your interactive shell. Macros aren't sandboxed — export FOO=bar inside a macro affects your shell, by design.
cdp <label> exec <command>… # jump, then run <command>
cdp <label> exec --save-macro=<name> <cmd>… # …and save it as a macrocdp api exec git status jumps to api and runs git status there — a one-shot macro you didn't have to define. The words after exec are joined with spaces and run in your live shell, so it composes exactly like a macro (env changes stick; interactive commands like git commit or a pager open on your terminal). Quote the command to use shell features that would otherwise be eaten by the outer shell: cdp api exec 'git log --oneline | head'.
Add --save-macro=<name> (right after exec) to persist the command as a Macro <name> under the project and run it now:
cdp api exec --save-macro=build npm run build
# → runs `npm run build` in api, and writes:
# Macro build
# Run npm run build
# thereafter reachable as: cdp api buildThe name must be a valid identifier, must not be a reserved subcommand or exec, and must not already name an action of the project (no silent overwrite — edit the config to change an existing macro). The save is atomic and re-verified before your config is replaced; the confirmation prints to stderr, so the command's own output is unaffected.
cdp add <label> <path> # add an entry
cdp add -g <group> <path> # add an entry into a group
cdp rm <label> # remove an entry
cdp path <label> # print the project's absolute path
cdp path -x <label> # print it and copy it to the clipboard
cdp ls # list projects (readable on a terminal)
cdp ls -l # force the readable layout when piping
cdp edit # open the config in $VISUAL / $EDITOR
cdp check # parse the config and report validitycdp add derives the project label from the directory's basename when you don't pass one, so the common case is just cdp add from inside the project. The path argument accepts ., a relative path, ~, or an absolute path and is canonicalized to an absolute path before it's written. If the basename isn't a valid label (e.g. it contains a .), pass one explicitly: cdp add <label> <path>. Forms: cdp add (current dir), cdp add <path>, cdp add <label> <path>.
Pass -g/--group <group> to attach the new project to a group — it's inserted as the group's last member, so it inherits the group's macros (and root, though the written Path is always absolute). The group must already exist; add --create-group to create it (a new Group block appended to the file) on the fly. The flag works in any position: cdp add -g work api ~/src/api or cdp add ~/src/api -g work. If the group is missing and you didn't pass --create-group, the add fails and the config is left untouched. As a safety measure cdp add re-parses the file it produced and verifies the new project resolves to the intended path and group before replacing your config.
On a terminal, cdp ls prints a readable per-project block — the path on its own line, and actions grouped by kind (macros, tmux, inherited):
cdp
path /home/user/xlnf/cdp
inherited claude (from xlnf)
xlnf
path /home/user/xlnf
macros deploy, logs
tmux dev
When the output is piped (not a terminal), cdp ls switches to a stable, awk-friendly LABEL\tPATH\tACTIONS line per project — each entry in ACTIONS is <name>:<kind> (<kind> is macro or tmux) in source order, with macros inherited from a Group (see below) carrying a trailing @<group> suffix (e.g. claude:macro@xlnf). Pass -l/--long to force the readable layout even when piping (e.g. cdp ls -l | less). Macros and tmux blocks have no dedicated subcommand — use cdp edit (or any editor) to add or modify them.
cdp path <label> writes the project's absolute path to stdout and nothing else, so it composes in command substitution — cd "$(cdp path api)", tar -C "$(cdp path api)" …, etc. (It's a pure lookup and does not require the directory to exist on disk.) Pass -x (or --clip) to also copy the path to the system clipboard; the path still prints, and a short confirmation goes to stderr. The clipboard backend is auto-detected in order — pbcopy (macOS), wl-copy (Wayland), xclip / xsel (X11), clip.exe (WSL) — or set $CDP_CLIPBOARD to a command of your own (it receives the path on stdin, e.g. CDP_CLIPBOARD='xsel -b'). If -x is given but no backend is found, the command prints the path and exits non-zero.
cdp edit resolves the editor in the standard $VISUAL → $EDITOR → vi chain. If the config file does not yet exist, the parent directory is created so the editor opens at the resolved path; the file is materialized on save.
cdp check runs the parser over the resolved config and reports whether it is valid — exit 0 and a brief OK line on stderr on success, exit 2 if the file is missing or unreadable, exit 65 with the parser's config:<line>: <message> on a parse error. Pairs naturally with cdp edit for an edit-then-validate loop, and is safe to use as a precondition: cdp check && cdp <label>.
cdp <label> <tmux-name> materializes a per-project tmux pane layout (or attaches if the session already exists). Each Tmux block names its layout with a small DSL — h:[…] for side-by-side splits, v:[…] for stacked splits, with arbitrary nesting — and one Pane <name> block per pane carrying the commands to send.
Project myapp
Path /home/user/myapp
Tmux dev
Layout h:[main | v:[test | logs]]
Pane main
Run pnpm dev
Pane test
Run pnpm test --watch
Pane logs
Run tail -f var/log/app.log
Then cdp myapp dev builds the layout (one main pane on the left; test over logs on the right) and attaches you. If the myapp-dev tmux session already exists, the user's running panes are kept as-is and cdp just attaches.
Direction convention follows tmux: h = horizontal divider (panes side-by-side), v = vertical divider (panes stacked). The full grammar, walk semantics, and protocol live in docs/specs/tmux-layout.md.
cdp init bash # prints shim source for bash
cdp init zsh # prints shim source for zsh (identical body in V1)Source it once per interactive shell, typically by adding eval "$(cdp init bash)" to your ~/.bashrc (or ~/.zshrc).
The same eval "$(cdp init bash)" line also installs tab completion — no extra setup. Re-source your rc file (or open a new shell) after upgrading to pick it up.
cdp <TAB> # subcommands + project labels
cdp web <TAB> # web's actions: macros, tmux blocks, inherited group macros, exec
cdp path <TAB> # project labels
cdp path -<TAB> # -x / --clip
cdp init <TAB> # bash / zsh
cdp add <TAB> # directoriesCandidates come from a hidden cdp __complete helper that reads your config, so completion always reflects the current file. zsh: completion is provided through bash-style bashcompinit, which needs zsh's own compinit to have run — if your ~/.zshrc doesn't already call compinit, add it before the eval "$(cdp init zsh)" line (cdp also attempts to bootstrap it defensively).
The config file lives at $CDP_CONFIG, or $XDG_CONFIG_HOME/cdp/config, or $HOME/.config/cdp/config — searched in that order.
Project myproject
Path /home/user/myproject
Macro deploy
Run cd ./static-pages
Run ./scripts/publish.sh
Macro logs
Run tail -f /var/log/myproject.log
Project api
Path /home/user/projects/api
Macro dev
Run pnpm install
Run pnpm --filter web dev
The formal grammar lives in docs/specs/config-format.md. Highlights: indentation-based blocks, case-insensitive keywords, # line comments only (no trailing comments), tilde expansion at parse time.
Wrap a set of related projects under a Group block to share Macros across them. A group's macros are inherited by every nested project; a project's own macro of the same name shadows the inherited one.
Group xlnf
Macro claude
Run ../xlnfclaude -c
Project xlnf
Path /home/user/xlnf
Macro claude # shadows the group's claude
Run ./xlnfclaude -c
Project cdp
Path /home/user/xlnf/cdp
# inherits claude from the group
Project shawarma
Path /home/user/xlnf/shawarma
# inherits claude
Project nexus # column 0 — outside any group
Path /home/user/nexus
Macro claude
Run claude -c
Then cdp cdp claude runs ../xlnfclaude -c (inherited), cdp xlnf claude runs ./xlnfclaude -c (override), and cdp nexus claude runs claude -c (no group affiliation). cdp ls shows the inheritance:
cdp /home/user/xlnf/cdp claude:macro@xlnf
nexus /home/user/nexus claude:macro
xlnf /home/user/xlnf claude:macro
Group rules in brief: nesting determines membership (one group per project); a Group carries Macros and, optionally, one Path root (see below) — Tmux and nested Group are parse errors; project-local always wins on name collision.
Add a single Path line directly under a Group (before its projects) to set a workspace root. Member projects are then resolved against it:
Group xlnf
Path /home/user/xlnf # the group's workspace root
Project xlnf # no Path → /home/user/xlnf (the root itself)
Project cdp
Path cdp # relative → /home/user/xlnf/cdp
Project notes
Path /opt/notes # absolute → /opt/notes (wins, escapes the root)
A relative member Path is joined onto the root; an absolute member Path wins (escapes the root); a member with no Path resolves to the root itself. The group root is tilde-expanded and must be absolute. A Group without a Path root behaves as before — its members must each declare an absolute Path.
- bash 4+ and zsh are tested daily.
- macOS ships bash 3.2 by default. Install a modern bash via Homebrew (
brew install bash) or use zsh (the system default on recent macOS). - fish and other shells are not supported in V1.
cdp: command not found — the eval line isn't in your rc file, or your rc file isn't being read by your shell. Confirm with type cdp; if it returns "not found", add the eval line and start a fresh shell.
cdp: requires bash 4 or newer — you're on macOS's default bash 3.2. Install bash 4+ (brew install bash) and exec it as your shell, or switch to zsh.
cdp: config file not found at ... — the config file doesn't exist yet. Run cdp add <label> <path> to bootstrap it; the parent directory will be created automatically.
cdp myproject runs but my cwd doesn't change — your shell's cdp is being shadowed by an alias or another function. Run type cdp; if it shows an alias, unalias cdp and re-source your rc file.
v1.11.0 — cdp <label> exec <command>… runs an ad-hoc command in a project (a one-shot macro), with stdin restored to the terminal so interactive commands work; --save-macro=<name> also persists the command as a Macro and runs it (save-and-run), atomically re-verified before the config is replaced. Built on the v1.10.x cdp add -g / v1.9.x tab completion / v1.8.x cdp path / v1.7.x readable cdp ls block / v1.6.x Group Path root / v1.5.x cdp add ergonomics / v1.4.x Group blocks / v1.3.x cdp check / v1.2.x cdp edit / v1.1.x tmux-integration line. The roadmap and open items are tracked as GitHub issues; the formal grammar and protocol live under docs/specs/.
See CONTRIBUTING.md.
MIT — see LICENSE.