Working with coding agents produces exhaust. Plans written before any code gets touched. Research notes on the library that didn't make the cut. A script that verified one thing once. Instructions telling the agent how I like to work.
None of it belongs in the repo, but all of it wants to live right next to the repo.
For a while, I handled this the usual way: a .gitignore entry and good intentions.
The problem is that .gitignore is a promise, not a guarantee. One git add -f when you're moving fast. One overly broad pattern change that un-ignores a directory as a side effect. One agent that "helpfully" cleans up your ignore file during a refactor—which is a real thing that happens when you give a language model write access to your tree.
Any of those can put your private notes into history. Once that history reaches a shared remote, cleaning it up means rewriting commits, coordinating with everyone who pulled them, and possibly rotating anything sensitive.
So I stopped trusting the promise. My notes now live one directory above the repo root, outside its worktree entirely:
myapp-workspace/ ← workspace, outside the Git repo
├── CLAUDE.md ← personal agent instructions
├── notes/ ← plans, research, session notes
├── scratch/ ← agent exhaust, one-off scripts
└── myapp/ ← the Git repo
└── CLAUDE.md ← optional, shared project instructions
Files in notes/ and scratch/ cannot be accidentally staged by commands operating on the repo. The protection doesn't depend on anyone remembering an ignore rule; the files are structurally outside the thing Git tracks.
What goes where
The workspace is a plain, untracked folder that wraps the repo.
notes/ holds anything durable: plans, research, and session summaries. scratch/ is the dumping ground for agent output and one-off experiments. Anything worth keeping graduates to notes/ or into the repo's own documentation. The repo itself is the only thing that ships.
CLAUDE.md is what makes the layout work well with agents. Claude Code loads instruction files from ancestor directories of wherever it starts, so a workspace-level CLAUDE.md is read by sessions inside the repo while remaining outside the repo's worktree.
Mine explains the layout and adds one important rule: never reference ../notes or ../scratch from anything committed inside the repo. The files can't be staged directly, but an agent could still copy or quote their contents. The instruction makes that boundary explicit.
The outer file doesn't replace a committed CLAUDE.md inside the repo. They serve different audiences: the workspace file holds personal preferences and rules specific to the wrapper, while an optional inner file can hold architecture, conventions, and gotchas that should travel with the code and be shared with the team. Claude reads both. Repoyard creates only the outer one; the inner file belongs to the project and its team.
Why the suffix goes on the outside
The workspace is named myapp-workspace, while the repo keeps the clean name myapp. I tried it the other way around first and walked it back for three reasons that all turned out to matter:
- Terminal tabs usually show the innermost folder. With the clean name inside, tabs show
myapprather than some variation ofworkspace. - Tools derive names from the repo directory. Docker Compose project names,
npm initdefaults, and similar conventions stay clean. - The repo directory matches the GitHub repository name, so running
git cloneinside a fresh workspace recreates the layout without a custom target argument.
The wrapper gets the suffix because the wrapper is the unusual part. The repo keeps the name every other tool expects.
The tool
I wrote a small CLI that scaffolds the convention: repoyard. It has zero dependencies, requires Node 20 or newer, and provides three commands:
npx repoyard create myapp # greenfield: workspace + fresh Git repo inside
npx repoyard adopt # wrap an existing repo
npx repoyard doctor # check a workspace against the convention
adopt is probably the command you'll use most, since most projects already exist.
Run it from the root of a clean repo and it creates the sibling workspace, moves the repo inside with a same-filesystem rename, and scaffolds the workspace files. If anything fails, it rolls the move back automatically. When it's done, it prints the cd command for the new location.
Run it interactively and it asks what you want using arrow-key menus. Pass --no-input to make it scriptable, or --dry-run to print the plan without touching anything. Pass --agent-file=claude|agents|both|none to scaffold AGENTS.md instead of—or alongside—CLAUDE.md, since the convention isn't Claude-specific.
The first repo I adopted was repoyard itself. The tool now lives inside the layout it scaffolds, which is either good dogfooding or a snake eating its tail, depending on your mood.
Yes, this is basically mkdir
You could set all of this up by hand in thirty seconds. The tool will not pretend otherwise.
What you're getting is the codified convention: the naming, the division between durable notes and disposable scratch work, and an agent-facing instruction file that explains the layout to any tool reading ancestor directories.
Conventions pay off when you, your agents, and your other machines all do the same thing without having to think about it. The syscalls were never the hard part.
The tool is also deliberately finished. There is no config file, plugin system, framework template, telemetry, or update checker. Worktrees already work fine inside a workspace:
git worktree add ../myapp-wip
Repoyard stays out of that too. Post-1.0 ideas have to solve a concrete problem, not merely exist.
If this failure mode has ever bitten you—or you'd prefer it never get the chance—you can find the project at github.com/ddyy/repoyard, or run npx repoyard in a scratch directory and try it yourself.
Top comments (0)