Skip to content

Repository files navigation

👻 ghostq

Your personal per-repo files, restored automatically on every clone and worktree.

CI npm version license: MIT runtime: Bun

Your personal files — .claude/ notes, .env.local, editor scratch configs — live in a separate overlay tree (a stable source of truth). ghostq symlinks them into the real checkout via a git post-checkout hook, so a fresh clone or git worktree add comes up with your personal files already in place.

No more re-creating .env.local by hand every time you clone. No more losing your scratch configs to a new worktree.

⚡ Quick start

ghostq ships as a single self-contained binary — no runtime needed on the machine that runs it. Build it once (see DEVELOPMENT.md) or grab a prebuilt binary, put it on your PATH, then:

ghostq install                    # one-time: hook future clones & worktrees

# bring a personal file for a repo you already have under ghostq:
cd path/to/your/repo
echo "TOKEN=..." > .env.local     # a gitignored, personal file
ghostq adopt .env.local           # moves it into the overlay + symlinks it back

Every fresh git clone or git worktree add of that repo now restores .env.local automatically. For a repo you cloned before installing ghostq, run ghostq apply in it once.

📖 Commands

Usage: ghostq [options] [command]

re-link personal, gitignored, per-repo files on clone / worktree add

Commands:
  install           install the post-checkout hook globally (init.templateDir)
  uninstall         remove the hook wiring
  apply [path]      link overlay files into the checkout (idempotent)
  adopt <files...>  move existing gitignored files into the overlay and link
                    them
  status [path]     show link states and warnings without changing anything
  prune [path]      remove dangling ghostq-managed links (idempotent)
  root              print the overlay root
  help [command]    display help for command

Adopting an existing file

If a personal file already exists as a real file in your checkout (instead of having been set up in the overlay first), ghostq adopt <file>... moves it into this repo's overlay entry and replaces it with the same kind of symlink apply would produce — one command instead of manually creating the overlay directory and copying the file over.

Each file passed to adopt must already be gitignored (adopt never adopts a tracked file) and must be a real file, not a symlink or a directory — pass individual files; ghostq never links whole directories (symlink-each). If the overlay already has a file at the target path, or the checkout path is already a symlink pointing elsewhere, adopt warns and skips it rather than clobbering anything.

🗂️ Overlay layout

Default overlay root: ${XDG_CONFIG_HOME:-~/.config}/ghostq/overlay, overridable with GHOSTQ_ROOT.

~/.config/ghostq/overlay/
└── github.com/
    └── simochee/
        └── ghostq/
            ├── .claude/
            │   └── personal.md      → symlinked into every checkout/worktree
            └── .env.local

Making the overlay root itself a git repo is recommended (not required) so your personal files are versioned and sync across machines.

🔧 How it works

  • On a fresh git clone or git worktree add, ghostq looks up an overlay entry by the repo's remote identity (host/user/repo) and symlinks its files into the checkout. An ordinary git switch / git checkout does nothing.
  • symlink-each: files are linked one by one, so a directory like .claude/ can hold committed files next to your overlay-linked personal files — whole directories are never symlinked.
  • Repos with no origin remote, or no matching overlay entry, are skipped quietly.

How the hook itself is wired (init.templateDir, the null-ref gate, migration from older versions) is an implementation detail — see DEVELOPMENT.md.

🤝 Coexisting with hook managers

ghostq doesn't take over your hooks. Install lefthook, husky, pre-commit, or your own .git/hooks scripts exactly as you normally would — there's nothing special to do, and nothing to undo. Hooks like pre-commit and pre-push coexist with no caveat.

Warning

git worktree add runs a single shared .git/hooks/post-checkout. If your own hook config also uses post-checkout (e.g. a post-checkout: block in lefthook.yml), it takes that slot and ghostq won't auto-link new worktrees of that repo — run ghostq apply in the new worktree by hand.
Clones, and every other hook, are unaffected.

A second, separate case: some managers set a repo-local core.hooksPath (husky does). git then reads hooks only from there, not .git/hooks, so ghostq's seeded hook never fires — for clones and worktrees both.
Check with git config --local --get core.hooksPath and unset it to restore ghostq.

🩺 Troubleshooting

Personal files not auto-linking on a fresh git clone or git worktree add? Work through these in order — each is a common reason the hook never seeds or never fires.

  1. Is ghostq installed? git config --get init.templateDir should print ghostq's template dir (path ending in ghostq/template).
    Empty, or pointing somewhere else, means ghostq install never ran (or something overwrote the setting) — run ghostq install and clone/worktree add again.

  2. Is a repo-local core.hooksPath set? git config --local --get core.hooksPath should print nothing; a path there shadows .git/hooks (where ghostq seeds its hook), so nothing fires.
    Some managers set it (husky does) — unset it (git config --local --unset core.hooksPath) or reconfigure that manager, then run ghostq apply once.

  3. Did you clone the repo before installing ghostq? git templates apply only at clone/init time and are never retroactive, so a checkout that predates ghostq install has no seeded hook.
    Run ghostq apply in it once to link the overlay by hand; future clones and worktrees are covered.

  4. Ask ghostq what it sees. ghostq status prints the resolved identity, the overlay entry path, and each file's link state, changing nothing — use it to confirm the repo maps to the entry you expect and to spot skipped files.
    A repo with no origin or no matching overlay entry is skipped quietly, which shows up here too.

🛡️ Safety

  • Before linking, each target path is checked with git check-ignore; a path that is not gitignored is warned about and skipped, so ghostq never dirties the working tree or sets up an accidental commit.
  • Apply is idempotent: already-correct links are skipped, and an existing real file that is not a ghostq-managed link is never clobbered — it is warned about and left alone.
  • apply only ever creates or repoints links; it never removes one, even after the overlay file behind it is deleted. ghostq prune cleans up the resulting dangling link — a symlink that still points into this repo's overlay entry but whose target no longer exists — and removes any parent directory left empty as a result. Live links and non-ghostq files are never touched.

🧭 ghq-shaped, not ghq-bound

The overlay tree mirrors the same host/user/repo layout that ghq uses — ghq's directory-structure philosophy is the direct inspiration for this tool, and credit goes to it.

That said, ghostq is standalone and has zero runtime dependency on ghq. It does not import ghq, shell out to it, or read ~/ghq; ghq does not need to be installed. ghostq arrives at the host/user/repo layout independently, by normalizing the repo's remote URL — the same transform ghq performs, computed on its own.

🚫 Non-goals

  • No dependency on or integration with ghq at runtime.
  • Not a general dotfiles / home manager. ghostq is specifically the per-repo overlay plus the auto-trigger on clone / worktree add.
  • Committed or shared files are out of scope — only personal, gitignored, per-repo files.

💛 Contributing

Build, test, and architecture notes live in DEVELOPMENT.md. Guidance for AI coding agents lives in AGENTS.md.

About

Restore your gitignored per-repo files automatically on every clone and worktree via a git hook.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages