Projj
Manage git repositories with directory conventions — clone once, find instantly.
The Problem
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.
The Solution
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
Install
# Cargo
# Homebrew (after first release)
Quick Start
Shell Integration
Add to ~/.zshrc (or ~/.bashrc, ~/.config/fish/config.fish):
| # fish
This sets up:
- Tab completions for all commands
projj runcompletes task names from[tasks]config and~/.projj/tasks/p()function for quick navigation
Commands
projj init
Initialize 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).
projj add <repo>
Clone a repo into the conventional directory structure.
Runs post_add hooks after cloning. Skips hooks if repo already exists.
projj find [keyword]
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
projj remove <keyword>
Remove a repo. Searches the same way as find, then requires typing owner/repo to confirm. Runs pre_remove / post_remove hooks.
projj run <task> [--all] [--match PATTERN] [-- ARGS...]
Run a task in the current directory, or all repos with --all.
projj list [--raw]
List all repositories with grouped display, colored by base directory and domain.
Configuration
~/.projj/config.toml
= ["/Users/x/projj", "/Users/x/work"]
= "github.com"
[]
= "git fetch && git pull origin -p"
= "rm -rf node_modules dist target"
= "git status --short"
[[]]
= "post_add"
= ["zoxide"]
[[]]
= "post_add"
= "github\\.com"
= ["zoxide", "git-config-user"]
= { = "popomore", = "me@example.com" }
[[]]
= "post_add"
= "gitlab\\.com"
= ["zoxide", "git-config-user"]
= { = "Other Name", = "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
Tasks are reusable commands that can be run manually via projj run or triggered by hooks.
Defining Tasks
Inline — one-liners in [tasks] table:
[]
= "git fetch && git pull origin -p"
= "rm -rf node_modules dist target"
Script files — executables in ~/.projj/tasks/:
Running Tasks
Resolution order: [tasks] table → ~/.projj/tasks/ file → raw shell command.
Task Context
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.
Built-in Tasks
Installed to ~/.projj/tasks/ on projj init.
zoxide
Registers the repo path with zoxide so z can jump to it. Silently skips if zoxide is not installed.
[[]]
= "post_add"
= ["zoxide"]
git-config-user
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).
[[]]
= "post_add"
= "github\\.com"
= ["git-config-user"]
= { = "popomore", = "me@example.com" }
[[]]
= "post_add"
= "gitlab\\.com"
= ["git-config-user"]
= { = "Other Name", = "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.
repo-status
Shows disk usage, git status, and ignored files for a repo.
Output 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
Hooks trigger tasks automatically at repo lifecycle events. They are the glue between events and tasks.
Events
| 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 |
Configuration
[[]]
= "post_add" # required: event name
= "github\\.com" # optional: regex on host/owner/repo
= ["zoxide", "git-config-user"] # required: tasks to run in order
= { = "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).
Matcher
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.
Environment Variables
| Variable | Description |
|---|---|
NO_COLOR |
Disable all colored output (no-color.org) |
PROJJ_HOME |
Override home directory for config location |
External Tools
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