Manage git repositories with directory conventions — clone once, find instantly.
Git repos pile up. You clone them into ~/code, ~/projects, ~/work, or wherever feels right at the moment. Six months later:
~/code/projj
~/projects/old-projj
~/misc/projj-backup
~/work/projj-fork
Which one is current? Where did you put that internal GitLab repo? You find / -name .git and wait.
Projj gives every repo a predictable home based on its URL — just like GOPATH did for Go:
$BASE/
├── github.com/
│ └── popomore/
│ └── projj/
└── gitlab.com/
└── company/
└── internal-tool/
- One repo, one location — no duplicates, no guessing
- Instant lookup — fuzzy find with fzf, jump with
p projj - Hooks — auto-configure git user, register with zoxide, run custom scripts on clone
- Multi-host — GitHub, GitLab, Gitee, self-hosted — all organized the same way
- Zero overhead — no daemon, no cache, no database, just your filesystem
# Cargo
cargo install projj
# Homebrew (after first release)
brew install popomore/tap/projjprojj init # one-time setup
projj add popomore/projj # clone → ~/projj/github.com/popomore/projj
projj add git@gitlab.com:team/app.git # clone → ~/projj/gitlab.com/team/app
p projj # jump to repo instantly (shell function)
projj run repo-status --all # batch operations across all reposAdd to ~/.zshrc (or ~/.bashrc, ~/.config/fish/config.fish):
eval "$(projj shell-setup zsh)" # zsh
eval "$(projj shell-setup bash)" # bash
projj shell-setup fish | source # fishThis sets up:
- Tab completions for all commands
projj runcompletes task names from[tasks]config and~/.projj/tasks/p()function for quick navigation
p projj # jump to projj
p egg # multiple matches → fzf selection
p # browse all repos with fzfInitialize configuration. Creates ~/.projj/config.toml, installs built-in tasks to ~/.projj/tasks/, and shows a summary of your setup (base directories, repos found, hooks, tasks).
Clone a repo into the conventional directory structure.
projj add popomore/projj # short form
projj add git@github.com:popomore/projj.git # SSH
projj add https://github.com/popomore/projj # HTTPS
projj add ssh://git@git.gitlab.cn:2224/web/cms.git # SSH with port
projj add ./local/repo # move local repoRuns post_add hooks after cloning. Skips hooks if repo already exists.
Find a repo by keyword (case-insensitive). Outputs the path to stdout.
- Single match — prints path directly
- Multiple matches — opens fzf for fuzzy selection with colored group tags (base/domain) and git URL
- No keyword — lists all repos for selection
- No fzf — falls back to numbered list
Remove a repo. Searches the same way as find, then requires typing owner/repo to confirm. Runs pre_remove / post_remove hooks.
Run a task in the current directory, or all repos with --all.
projj run "npm install" # raw command
projj run update --all # named task in all repos
projj run "git status" --all --match "SeeleAI" # filter repos by regex
projj run repo-status -- --detail # pass args to task after --List all repositories with grouped display, colored by base directory and domain.
projj list # pretty: grouped by base/host, colored, with git URL
projj list --raw # plain paths, one per line (for piping)~/.projj/config.toml
base = ["/Users/x/projj", "/Users/x/work"]
platform = "github.com"
[tasks]
update = "git fetch && git pull origin -p"
clean = "rm -rf node_modules dist target"
status = "git status --short"
[[hooks]]
event = "post_add"
tasks = ["zoxide"]
[[hooks]]
event = "post_add"
matcher = "github\\.com"
tasks = ["zoxide", "git-config-user"]
env = { GIT_USER_NAME = "popomore", GIT_USER_EMAIL = "me@example.com" }
[[hooks]]
event = "post_add"
matcher = "gitlab\\.com"
tasks = ["zoxide", "git-config-user"]
env = { GIT_USER_NAME = "Other Name", GIT_USER_EMAIL = "other@corp.com" }| Field | Description | Default |
|---|---|---|
base |
Root directory (string or array) | ~/projj |
platform |
Default host for short form owner/repo |
github.com |
tasks |
Named tasks (see Tasks) | {} |
hooks |
Event-driven hooks (see Hooks) | [] |
Tasks are reusable commands that can be run manually via projj run or triggered by hooks.
Inline — one-liners in [tasks] table:
[tasks]
update = "git fetch && git pull origin -p"
clean = "rm -rf node_modules dist target"Script files — executables in ~/.projj/tasks/:
cat > ~/.projj/tasks/notify << 'EOF'
#!/bin/bash
echo "Added $PROJJ_REPO_OWNER/$PROJJ_REPO_NAME"
EOF
chmod +x ~/.projj/tasks/notifyprojj run update --all # inline task
projj run notify --all # task file
projj run repo-status -- --detail # pass arguments after --
projj run "git log -5" # raw command (not a named task)Resolution order: [tasks] table → ~/.projj/tasks/ file → raw shell command.
When tasks are executed via hooks, they receive repo context via environment variables:
PROJJ_EVENT — event name (e.g. post_add)
PROJJ_REPO_PATH — full path to repo
PROJJ_REPO_HOST — e.g. github.com
PROJJ_REPO_OWNER — e.g. popomore
PROJJ_REPO_NAME — e.g. projj
PROJJ_REPO_URL — e.g. git@github.com:popomore/projj.git
These are system-provided variables. Hooks can also pass custom variables via the env field (see Hooks).
JSON is also sent via stdin for richer parsing. When run manually via projj run, these variables are not set.
Installed to ~/.projj/tasks/ on projj init.
Registers the repo path with zoxide so z can jump to it. Silently skips if zoxide is not installed.
[[hooks]]
event = "post_add"
tasks = ["zoxide"]Sets user.name and user.email for the repo. Reads from custom env vars set in the hook's env field (not system-provided PROJJ_* variables).
[[hooks]]
event = "post_add"
matcher = "github\\.com"
tasks = ["git-config-user"]
env = { GIT_USER_NAME = "popomore", GIT_USER_EMAIL = "me@example.com" }
[[hooks]]
event = "post_add"
matcher = "gitlab\\.com"
tasks = ["git-config-user"]
env = { GIT_USER_NAME = "Other Name", GIT_USER_EMAIL = "other@corp.com" }| Env var | Description |
|---|---|
GIT_USER_NAME |
Value for git config user.name |
GIT_USER_EMAIL |
Value for git config user.email |
Both are optional. Skips if not set.
Shows disk usage, git status, and ignored files for a repo.
projj run repo-status # current repo
projj run repo-status --all # all repos (quick summary)
projj run repo-status -- --detail # include ignored files breakdownOutput example:
📦 1.1G total | 🗃️ .git 2.0M | ✓ clean
📦 1.1G total | 🗃️ .git 2.0M | ✓ clean | 🚫 15437 ignored: target(1.1G, 99%)
Colors by size: green (<100M), yellow (100M–1G), red (>1G). Respects NO_COLOR.
Hooks trigger tasks automatically at repo lifecycle events. They are the glue between events and tasks.
| Event | When | cwd |
|---|---|---|
pre_add |
Before clone/move | Target directory |
post_add |
After clone/move | Repo directory |
pre_remove |
Before deletion | Repo directory |
post_remove |
After deletion | Parent directory |
[[hooks]]
event = "post_add" # required: event name
matcher = "github\\.com" # optional: regex on host/owner/repo
tasks = ["zoxide", "git-config-user"] # required: tasks to run in order
env = { GIT_USER_NAME = "popomore" } # optional: custom env vars for tasks| Field | Required | Description |
|---|---|---|
event |
Yes | Event name |
matcher |
No | Regex against host/owner/repo. Omit to match all |
tasks |
Yes | List of task names or commands, executed in order. Stops on first failure |
env |
No | Custom environment variables passed to tasks (user-defined, not PROJJ_*) |
Each entry in tasks is resolved the same way as projj run (task table → task file → raw command).
The matcher field is a regex matched against host/owner/repo. Omit to match all repos.
| Matcher | Matches |
|---|---|
(omitted) or * |
All repos |
github\\.com |
All GitHub repos |
github\\.com/SeeleAI |
All repos under SeeleAI org |
github\\.com/popomore/projj |
Exact repo |
gitlab\\.com|gitee\\.com |
GitLab or Gitee repos |
Note: . in regex matches any character. Use \\. to match a literal dot.
| Variable | Description |
|---|---|
NO_COLOR |
Disable all colored output (no-color.org) |
PROJJ_HOME |
Override home directory for config location |
Optional integrations. Projj works fine without them.
| Tool | Integration | Without it |
|---|---|---|
| fzf | Fuzzy search in find / remove |
Numbered list |
| zoxide | post_add hook registers paths |
No auto-registration |
# macOS
brew install fzf zoxide