I keep some files inside my project’s working directory that don’t belong to the project. Editor config. Claude Code settings under .claude/. A justfile.local with maintainer-only commands. They’re useful only to me, but I want them version-controlled — across machines, with history.
They can’t go in the public repo. I needed a way to track them that stays out of the public history, stays out of contributors’ way, and stays restorable on a fresh machine.
Here’s the shape that works for me.
The shape#
- the project repo is public-ready
- the chezmoi repo holds dotfiles and project-local private files
- the project’s
.gitignoreexcludes the private paths - the public
justfileoptionally importsjustfile.local - chezmoi writes the private layer into the project’s working tree
On disk, public and private files live side by side. In git history, the project repo sees only the public ones.
Walk-through#
The project on disk:
telegram-rust-cli/
src/
README.md
Cargo.toml
justfile
.gitignore
.claude/ # ignored by project git, tracked by chezmoi
justfile.local # ignored by project git, tracked by chezmoiThe project .gitignore:
.claude/
justfile.local
config.toml
.envThe public justfile:
set shell := ["bash", "-cu"]
import? 'justfile.local'
default:
@just --list
fmt:
cargo fmt --check
test:
cargo test
clippy:
cargo clippy --all-targets --all-features -- -D warnings
audit:
cargo audit
fast-check: fmt clippy
check: fmt test clippy auditThe line that makes this work:
import? 'justfile.local'The ? makes the import optional. Contributors and CI don’t need justfile.local. If the file is missing, just continues normally. On my machine, the file exists and adds private commands.
My justfile.local:
chezmoi_source := `chezmoi source-path`
private_claude := justfile_directory() / ".claude"
private_justfile := justfile_directory() / "justfile.local"
private-help:
@echo "Private project state is managed by chezmoi."
private-status:
git -C "{{chezmoi_source}}" status -sb
private-diff:
chezmoi diff "{{private_claude}}"
chezmoi diff "{{private_justfile}}"
private-save:
chezmoi re-add "{{private_claude}}"
chezmoi re-add "{{private_justfile}}"
git -C "{{chezmoi_source}}" status --short
private-commit message:
git -C "{{chezmoi_source}}" add .
git -C "{{chezmoi_source}}" commit -m "{{message}}"
private-push:
git -C "{{chezmoi_source}}" push
In the chezmoi source repo, the same files live under the project’s path:
chezmoi-dotfiles/
git-repos/
telegram-rust-cli/
dot_claude/
settings.json
justfile.localDay-to-day#
After editing private files I run:
just private-diff
just private-save
just private-commit "Update private project settings"
just private-pushOn a new machine or after a fresh clone of the project at the same path:
chezmoi applyBoth .claude/ and justfile.local come back where they belong, and the project’s public commands stay unchanged.
Why chezmoi fits#
Chezmoi is usually pitched as a dotfiles manager, but it’s more accurate to call it a personal machine-state manager. It can track files at any path on your machine, with any name. Project-local files inside a public project’s working tree are fair game.
The two-repo alternative#
Another shape is to keep two repos for the same project — a private development one and a public release one — and snapshot between them. I haven’t lived with that workflow so I won’t argue against it. It just looked less convenient to me, since every change forces a “which repo?” decision.
The split that makes it work#
The public project should be understandable without my private environment. My private environment should be restorable without polluting the public project.
Optional imports, .gitignore, and chezmoi are enough to make that split practical.
