The first place I want to learn that a change is broken is the machine where I made it.
Not because the laptop is more trustworthy than CI. It is not. I want local checks because a failure is cheaper while the editor, test, logs, and reason for the change are still in front of me.
Local first does not mean local only. The useful model is fast local feedback followed by independent CI confirmation.
Waiting changes how people work
A two-second failure feels like part of editing. A four-minute failure becomes another task.
That delay creates familiar commit chains:
fix lint
fix the type error CI found
actually include the generated file
The problem is not untidy history. It is that every loop requires a push, a queue, a context switch, a notification, and a return trip through logs. Developers start batching larger changes because each attempt is expensive. Larger batches are harder to diagnose.
Fast feedback keeps the unit of change small.
Local failures have better context
When a test fails locally, I can inspect state, rerun one case, add a log, open the source, and compare the diff without reconstructing the environment from an artifact.
CI gives me a log and an exit code. That is exactly what I want from an independent verifier. It is a poor interactive debugger.
If the next step after every CI failure is “run this locally,” the check belonged in the local workflow too.
Watch mode is especially valuable here. A type checker, unit-test runner, or source scanner that stays warm can report on each save faster than a hook that starts cold at commit time.
CI owns the trust boundary
Local tools run on a developer-controlled machine. They can be missing, misconfigured, stale, or bypassed. CI starts from the committed inputs and applies the policy the shared branch requires.
That makes CI the right place to guarantee:
- Clean dependency installation from the lockfile
- Supported runtime and operating-system matrix
- Complete test suite
- Integration services and controlled credentials
- Production build and artifact checks
- Required security, licensing, or policy gates
- Deployment authorization
A green CI result should confirm what the developer expected. It should still be able to surprise them when their machine hid a missing file, platform difference, or undeclared dependency.
The independence is the point.
Split checks by feedback cost
I use three layers.
While editing
- Formatter and linter integration
- Incremental type checking
- Focused tests in watch mode
- Fast source or configuration checks
These can report before a commit exists.
Before commit or push
- Staged-file lint and format check
- Project or package type check
- Tests related to changed files
- Full unit suite before push when it stays reasonably fast
- Dependency or source audit appropriate to the diff
The pre-commit hook setup shows how I keep the fastest layer useful without turning every commit into a release rehearsal.
In CI
- The required checks above, repeated from a clean checkout
- Broader integration and platform coverage
- Build, packaging, and deployment-specific verification
Repeating a test is not waste when the second environment proves a different property.
Keep local and CI commands the same
The worst setup has one script developers run and a hand-written CI workflow that does something almost, but not quite, equivalent.
Put the real commands in the repository’s package scripts or task runner. Let hooks and CI call those commands. Keep runtime versions and package-manager settings committed. If CI needs a special flag, make its purpose explicit.
This gives a failed job a useful reproduction command:
pnpm quality
Not “read 80 lines of workflow YAML and approximate the five shell steps.”
Some checks naturally stay in CI
I do not need every developer running every service and platform locally.
Good CI-only candidates include:
- A browser matrix that covers engines the team does not use daily
- Tests requiring short-lived service credentials
- Expensive integration environments
- Release signing and packaging
- Deployment smoke tests from outside the hosting network
Even then, keep a smaller local substitute when practical. A unit-level contract test cannot prove the external integration works, but it can catch a malformed request before the remote job runs.
The goal is not ideological purity. It is paying the slow feedback cost only when the evidence requires it.
Measure the loop
Record how long the local and CI paths take. Look at the checks developers bypass and the failures CI finds first.
- If CI repeatedly catches formatting, the local hook is missing or unreliable.
- If local tests pass but clean installs fail, dependency inputs are incomplete.
- If developers skip a 90-second commit hook, move work to pre-push or make it incremental.
- If a flaky integration job blocks every change, fix the flake instead of normalizing reruns.
Workflow quality is observable. The commit history and CI queue will tell you where feedback arrives too late.
SiteCMD uses this model in its local-first architecture: run website and source checks on the machine, keep the evidence local, and use CI or pre-push gates when a repository needs shared enforcement. How SiteCMD scans work explains which checks stay entirely on-device and which live-site checks must request the public URL.
The laptop finds the problem quickly. CI proves the repository did not depend on that laptop.