Code Quality Gates for Vibe-Coded Projects
My friend is vibe coding a “personal OS” for their new job. The whole thing is written in Go. He doesn’t know Go and hasn’t looked at the code. “But how will he know it works?” He’s using it and it works. That’s about it.
Less facetiously, he shared his CLAUDE.md with me recently. It’s pretty solid. It looks very similar to what I did when working on uniffi-bindgen-node-js. I talked a bit about this in simplifying AI-generated code with Lizard.
CLAUDE.md
## Code Quality Standards
### Pre-commit Hooks (enforced automatically)
Pre-commit is configured (`.pre-commit-config.yaml`). Every commit runs:
- `go fmt` + `go imports` — formatting
- `go build` — compilation check
- `go test` — unit tests must pass
- `golangci-lint` — expanded linter set (see below)
- `trailing-whitespace`, `end-of-file-fixer`, `check-yaml` — hygiene
- `detect-private-key` — no secrets in commits
### Linting (`.golangci.yml`)
The project uses an expanded linter set beyond defaults:
- **errcheck** — unchecked errors
- **govet**, **staticcheck**, **gosimple** — correctness
- **gocritic** — style, performance, diagnostics
- **gosec** — security vulnerabilities
- **goconst** — repeated magic strings
- **prealloc** — slice preallocation
- **nilerr** — returning nil when err is set
- **bodyclose** — unclosed HTTP response bodies
- **noctx** — HTTP requests without context
- **copyloopvar** — loop variable capture
Run: `make lint` or `golangci-lint run ./...`
### Test Coverage
**Target: 90%+ on `internal/` packages. This is a hard floor, not a goal.**
**Coverage must not regress.** Every new feature, handler, API endpoint, or store method MUST include tests in the same commit.
**Enforcement rule:** After writing any code, run `make cover` and check per-package coverage. If any package dropped below 90%, write tests to restore it before committing.
- Use table-driven tests for functions with multiple cases
- Use `httptest` for handler/API tests
- Use temp SQLite databases (`t.TempDir()`) for store tests
- Test edge cases: empty inputs, not-found, error paths
- New API endpoint → add request/response test in `api_test.go`
- New page handler → add 200-status test in `handlers_test.go`
- New store method → add CRUD test in `store_test.go`
- Smoke test against real vault: `make smoke`
### Complexity
**Max cyclomatic complexity per function: 15.** Check with `gocyclo -over 15 .` — should return empty.
When a function exceeds 15:
- **Extract helpers** — move cohesive blocks (filtering, scoring, parsing) into named functions
- **Use table-driven patterns** — replace repetitive if/switch blocks with data-driven loops
- **Accept structs, not long parameter lists** — if a function takes 5+ args of the same type, use a struct
- **Don't share helpers between callers with different needs** — two similar-looking loops that build different outputs are not duplication
### Verification Checklist (before any commit)
```bash
make check # runs: lint → test → cover (all three)
gocyclo -over 15 . # must return empty
Makefile Targets
| Target | Purpose |
|---|---|
make build |
Compile binary to bin/harness |
make test |
Run all tests |
make lint |
Run golangci-lint |
make cover |
Run tests with coverage report |
make check |
lint + test + cover (full quality gate) |
make smoke |
Smoke test against real vault |
make security |
govulncheck + gosec |
make run |
Build, stop existing, start in background |
make restart |
Same as run (rebuild + restart) |
make stop |
Kill running harness |
make logs |
Tail the log file |
Architecture Conventions
- Vault as backend — the Obsidian vault is the source of truth for domain data. The harness reads
.mdfiles and builds an in-memoryIndex(protected bysync.RWMutex). - SQLite for operational state — jobs, schedules, alert acks, integration cache, metrics. NOT for vault data.
- Skills via Claude CLI — external actions (Slack, Jira, Calendar) go through Claude Code skills, not native Go HTTP clients. Exception: 15Five uses a native client (no MCP tool available).
- Dynamic skill registry — skills are loaded from
$VAULT_PATH/.claude/commands/*.mdat startup. Add a new.mdfile withdescription:andcategory:frontmatter → it appears on the Skills page without a restart. - Feature flags — integrations are gated by env vars (
ENABLE_GCAL,ENABLE_JIRA,ENABLE_SLACK,ENABLE_15FIVE). Handlers check flags before reading cache. - Write-back pattern — the harness can write to vault files (task completion, project status, notes). Use
os.ReadFile→ transform →os.WriteFile. The file watcher detects changes and triggers index rebuild. - Catppuccin Mocha theme — all templates use
ctp-*Tailwind classes. Colors defined intailwind.configinlayout.html.
File Organization
internal/vault/ — parsing, indexing, types (vault read-only)
internal/store/ — SQLite state (jobs, schedules, cache, queue)
internal/web/ — HTTP handlers, API, SSE, templates
internal/skills/ — skill runner, scheduler, registry, post-processor
internal/alerts/ — alert evaluation rules
internal/fifteenfive/ — 15Five API client
internal/metrics/ — code quality metric collector
Harness-Specific Recovery
Skills like /prep-bob-1on1, /weekly-rollup, and /morning query multiple MCP sources and can take 2+ minutes. If any single query hangs or times out, skip it and note “Data unavailable: [source]” in the output.
Adding New Features
- If it reads external data → create a Claude Code skill in
~/my-project/.claude/commands/withdescription:andcategory:frontmatter. It auto-registers via the dynamic registry. Use theharness-cachepost-processor pattern for caching. - If it needs persistent state → add a SQLite table in
store.gomigrate() - If it needs a UI → add handler in
handlers.go, template intemplates/pages/, route inserver.go, nav link innav.html - If it modifies vault files → follow the write-back pattern (read → transform → write, watcher handles rebuild)
You run tests as the last step if you make any code changes.
The trick with vibe coded projects (that are actually meant to be used) is to come up with a very robust definition of quality:
- Code works
- Code is fast
- Code does what I want
- Code doesn’t fail
And so on. Then you tell the LLM ways to measure these things. It really comes down to the same stuff we’ve always been doing, just a lot more of it.
- Unit tests
- Integration tests
- System tests
- Performance tests
- Soak tests
- Leak tests
- Chaos tests
- Cyclomatic complexity
- Lint
- Code smells
- Code style
- Security tests
- … and so on
Then you define acceptable criteria:
- 90%+ unit test coverage on internal packages
- No cyclomatic complexity over 15
- No lint errors
- No code smells
- No security issues
- Performance benchmarks must not regress
- … and so on
Then you make sure your agent runs these checks, and you’re in pretty good shape.