To Yak Shave or Not: The Art of Procrastination in Software Development
You sit down to ship a chat component. Four hours later you’ve upgraded TypeScript, fixed three unrelated type errors, bumped your test runner, and rewritten your ESLint config. The chat feature has zero lines of code. Welcome to yak shaving.
The interesting question isn’t “how do I stop?” It’s “which yaks are worth shaving, and which should I leave alone?”
Yak shaving vs. bike shedding
I conflated these for years, so let’s nail the distinction first.
- Yak shaving is a dependency chain. Task A requires B requires C requires shaving a yak. The tasks are usually real and usually solo.
- Bike shedding is misallocated attention. A group spends 40 minutes on the button color and 5 minutes on the auth model. The tasks are trivial and usually social.
Quick test: if you’re alone going deeper, it’s a yak. If you’re in a meeting going wider on something small, it’s a shed.
They share a root (losing the plot), but the fixes differ. Yak shaving is fixed by scoping and stashing. Bike shedding is fixed by forcing a decision (“rename in a follow-up PR; merging now”) or a default (“formatter wins, move on”). The rest of this post is about yaks.
A decision framework: shave, stash, or skip
When you hit a yak, run three checks before touching it:
- Is it actually blocking? Not “would be nicer,” but “I cannot ship without this.” If the answer is no, it’s not on the critical path.
- Is the fix bounded? Can you size it in minutes, or does it dissolve into “let’s see”? Unbounded yaks eat days.
- Does it belong in this PR? A reviewer should be able to describe your diff in one sentence. If the yak breaks that sentence, it belongs in its own PR.
Three outcomes:
- Shave it now if it’s blocking and bounded and small enough to fit the PR’s story.
- Stash it (separate branch, separate PR, do it next) if it’s blocking but doesn’t belong, or bounded but not blocking.
- Skip it (file an issue, move on) if it’s neither blocking nor bounded.
Most yaks I regret were category-three pretending to be category-one.
The chat example, replayed
Same scenario, run through the framework:
- Outdated TypeScript. Blocking? No. The new utility types are nice, not required. Skip. File an issue: “Bump TS, evaluate
satisfiesfor X.” - TS upgrade breaks types. Wouldn’t have happened; we skipped step 1. This is the cascade you’re avoiding.
- Failing tests. Blocking if they’re tests for chat. Skip if they’re pre-existing failures on
main. Don’t fix unrelated red tests in a feature PR; that PR can never be reverted cleanly. - ESLint misconfig. Skip. File it. Linter setup is its own PR with its own review.
The chat ships in an afternoon. The yaks become a backlog you can prioritize on their own merits, instead of a pile you discovered at 11pm.
Tactics that actually work
Keep a yak file open. A scratch buffer, a TODO at the bottom of the PR description, anywhere you can flush a distraction in 10 seconds and return to the task. Most yaks die quietly once written down; they only metastasize when you’re afraid you’ll forget them.
git stash and git worktree are yak insurance. When a “quick fix” starts touching unrelated files, stash, branch, or open a worktree. Cheap to do, expensive to skip. A clean diff is a forcing function: if your fix doesn’t fit, you’ll feel it.
Time-box with a real timer. “I’ll spend 20 minutes on this” without a timer means an hour. Set one. When it goes off, the question isn’t “am I close?” but “do I stash or commit another 20?”
Notice the second hop. Yak 1 is usually fine. Yak 2 is the warning. By Yak 3 you are no longer doing the original task; you are doing archaeology. The pattern to recognize is the hop, not the depth.
Revert is your escape hatch. If you’ve gone three hops in and the original feature is untouched, git reset to the start of the session and try again with the framework. The hour is gone either way; don’t compound it.
When to shave anyway
The framework says skip; sometimes you should still shave. Two cases:
- The yak will block you again next week. Flaky CI, broken local dev, a type error you keep working around: the cost compounds. Pay it down once, in its own PR, but pay it.
- You’re learning the codebase. Early in a new project, following the chain teaches you the system. The “wasted” time is onboarding. This expires fast; once you’re oriented, the framework applies.
Outside those, the boring answer wins: file the issue, ship the feature, let the yak grow its coat back.
Stay in touch
Don't miss out on new posts or project updates. Hit me up on X for updates, queries, or some good ol' tech talk.
Follow @zkMake