Skip to content

Latest commit

 

History

History
127 lines (82 loc) · 14 KB

File metadata and controls

127 lines (82 loc) · 14 KB

CLAUDE.md

This file provides guidance to AI coding agents — Claude Code (claude.ai/code) and vendor-neutral tools such as Codex, OpenCode, Cursor, and Copilot — when working with code in this repository.

Agent instruction files

CLAUDE.md and AGENTS.md are kept byte-identical. CLAUDE.md is what Claude Code reads; AGENTS.md is what vendor-neutral agent tools read — Codex, OpenCode, Cursor, Copilot, and whatever follows them. Two real files, deliberately not a symlink: not every tool resolves one.

After editing either file, copy it over the other — don't repeat the edit by hand:

cp CLAUDE.md AGENTS.md   # or the reverse, whichever you just edited

Retyping a change is exactly how the two drift; one reflowed line or reworded clause is enough. diff CLAUDE.md AGENTS.md must print nothing. If it ever does, treat it as a defect and fix it by letting one file win wholesale — never by merging them.

What this repo is

scaffold is a GitHub template repository, not an application. It ships the meta layer (lint, format, commit hooks, CI, CodeQL, Dependabot, release-please, issue/PR templates, standard meta docs) that every new kirchDev repo should start with. There is no application code — the project code can be anything (PHP, Go, Rust, Vue, shell). Only the meta layer lives here.

Implication: when changing files, ask "does this default make sense for every future repo created from this template?" — not just for one project type.

Commands

Command What it does
pnpm install Install deps and wire husky hooks via the prepare script
pnpm lint oxlint . --deny-warnings
pnpm format oxfmt --check . (note: format is the check, not fix)
pnpm check Runs lint + format + check:policy — the CI gate
pnpm check:policy Proves the two agent policy files ban the same commands
pnpm lint:fix Auto-fix lint
pnpm format:fix Auto-fix format
pnpm check:fix Auto-fix lint + format
pnpm skills:update Update project-scoped agent skills via the skills.sh CLI
pnpm taze Interactive dependency upgrade check
pnpm taze:w Write upgrade results

There is no test suite — this is config-only. CI runs pnpm lint, pnpm format and pnpm check:policy on PR.

Architecture / conventions

  • Node 24, pnpm 11. Pinned via .nvmrc, engines, and packageManager. pnpm-workspace.yaml enforces minimumReleaseAge=4320 (3-day cooldown), isolated node-linker. Don't loosen these without reason.
  • oxc, not eslint/prettier. Linting via oxlint, formatting via oxfmt. Configs live in .oxlintrc.json / .oxfmtrc.json. oxlint uses unicorn + oxc plugins; rules deliberately minimal.
  • Husky hooks (.husky/pre-commit, .husky/commit-msg) run lint-staged and commitlint. lint-staged.config.js excludes README.md, CLAUDE.md, and AGENTS.md (free-form prose) and pnpm-lock.yaml. oxlint --fix --deny-warnings then oxfmt on JS; oxfmt only on JSON/YAML/MD.
  • Conventional Commits enforced via @commitlint/config-conventional. Don't --no-verify unless explicitly asked.
  • release-please is included (unlike many templates that omit it). Files: release-please-config.json, .release-please-manifest.json, .github/workflows/release-please.yml. Config uses release-type: simple (language-agnostic), include-v-in-tag: true. Downstream repos start at 0.0.0 and reset via the steps in README → Resetting release-please.
  • Workflows use actions/checkout@v6, actions/setup-node@v6, pnpm/action-setup@v6, github/codeql-action/{init,analyze}@v4. Keep these pinned to major versions; Dependabot bumps them monthly.
  • CodeQL scans actions + javascript-typescript with security-extended,security-and-quality queries, gated by path filters so non-code changes don't trigger it.
  • Dependabot groups all minor/patch updates per ecosystem into a single PR (npm-minor-patch, actions-minor-patch). Majors come as separate PRs.

AI & skills

  • .claude/settings.json ships a baseline permission policy — see Permission policy below for the rules it follows. .claude/settings.local.json (per-machine overrides, typically enabledMcpjsonServers) is gitignored.
  • .tituskirch-skills.json configures the TitusKirch skills (commit, PR, issue, release, docs …) per repo. It is the runtime config, not an installer. Regenerate/reconcile it with the tituskirch-skills-config skill.
  • Installing the skills. The bundle is installed via the skills.sh CLI (pnpm dlx skills add TitusKirch/skills), not vendored into the repo. pnpm skills:update refreshes project-scoped skills tracked in skills-lock.json (only present once a repo actually installs project skills).

Permission policy

.claude/settings.json is deliberately lopsided: a long deny list and a short allow list. The two sides answer different questions, so they follow opposite rules.

deny may be generous. A rule for a command the repo doesn't have is a no-op, it never needs maintenance, and it is never reviewed — a too-broad block only surfaces when you actually hit it. So the list covers every stack kirchDev repos might grow into (Laravel, Prisma, Terraform/OpenTofu, AWS), not just this one. git reflog expire and git gc --prune=now are in there because they destroy the rescue path that survives a reset --hard.

The line to draw is the machine or something remote, not the working copy. Blocked: anything that wrecks the OS (dd, mkfs, chmod -R, rm -rf /…), tears down remote state or resources (terraform destroy, state rm, aws ec2 terminate-instances, gh repo delete), or throws away work with no recovery path (force-push, reset --hard, stash drop). Deliberately not blocked, because they are ordinary local development: rm -rf node_modules, docker volume rm, docker compose down -v, docker system prune, php artisan tinker, deleting a remote branch. Those prompt instead — a command that is sometimes wanted belongs in the middle state, never in deny.

allow must stay short. Its only return is fewer prompts — no safety is gained. Every line has to be read and understood by whoever copies this file, and an unreviewed allow list is more dangerous than none. Keep what occurs many times per session (read-only git, ls/grep/rg, the project's own check scripts) and let everything else ask.

Three states, not two. A command in allow runs unasked; one in deny is impossible and has to be typed by hand; one in neither list prompts you — and that middle state is the right default for almost everything. Reserve deny for what a mistaken "yes" could not undo. A normal git push is not that: it is reversible, visible and the ordinary way work ships, so it sits in allow.

Important

Never allow a rule that runs arbitrary code. php artisan tinker --execute, pnpm exec turbo run, find . * (which covers -delete and -exec rm), a raw pnpm dlx, or an MCP tool that executes SQL (database-query, run-query) each hand back everything the deny list took away — a blocked db:wipe means nothing next to an allowed tinker --execute 'DB::statement(...)'. A deny list is only as strong as the weakest allow rule beside it.

Two things this file cannot do, by design: it cannot tell which branch a git push targets (protect release branches with branch protection, not permissions), and prefix rules miss flags placed before the subcommand (docker compose -f x.yml down -v). Treat it as lowering the odds, not as a guarantee.

Downstream repos keep the deny list as-is and swap the pnpm lines in allow for whatever their stack runs.

Codex gets the same policy in .codex/rules/default.rules — permission config is not portable, so the block list exists twice and both must be changed together. Codex uses Starlark prefix_rule() calls matching on argument tokens, which handles flags and shell chains that the Bash(…) prefix patterns miss, and every rule carries its own match/not_match cases. Check a rule with:

codex execpolicy check --pretty --rules .codex/rules/default.rules -- git push --force

Parity between the two is machine-checked, not eyeballed. pnpm check:policy (scripts/check-policy-parity.js, part of pnpm check and of CI) expands every prefix_rule into its concrete argv prefixes — the cartesian product over its alternation lists — and matches the two sets in both directions, so "we changed both files" becomes a number rather than a claim. Two things it encodes are worth knowing before editing either file:

  • The languages differ, so a few gaps cannot be closed. Claude Code matches a prefix of the command string; a prefix_rule matches whole argv tokens. Bash(aws iam delete-:*) therefore bans every delete verb AWS will ever ship, and the Codex side can only enumerate the ones it ships today. Such a difference is legal but must be declared — in the DELIBERATE list in the script and in the .codex/rules/default.rules header — and the check fails both on an undeclared one and on a declaration that has gone stale.
  • Neither language normalises flag order or case. rm -rf / and rm -fr / are separate bans; rm -r -f / and redis-cli FlushAll are neither, and enumerating permutations never ends. The check proves the two files list the same spellings — it does not claim the set of spellings is complete. Same caveat as the two below, and for the same reason.

Branching model

The default here is a dev integration branch: branch off dev, PR into dev, roll dev up into main, and release-please releases from main. That is what most kirchDev repos run, so the template runs it too — a variant that ships switched off is a variant nobody notices is broken.

Important

A repo created from this template has the dev config but no dev branch. Create it before the first Dependabot run: with target-branch: 'dev' pointing at a branch that doesn't exist, Dependabot opens nothing at all. Going main-only (below) is a deliberate step too — leaving the config untouched is the one option that silently does nothing.

.github/workflows/dev-pr.yml opens and updates the rolling draft devmain PR. Mark that PR ready and merge it with a merge commit, never a squash: squashing collapses the individual feat:/fix: commits into the PR's own chore: title, and release-please then cuts nothing.

Going main-only is three edits, all of them removals:

rm .github/workflows/dev-pr.yml
# .github/dependabot.yml    — drop both `target-branch: 'dev'` lines
# .tituskirch-skills.json   — set `pr.base` to "main"

Nothing is vendored for this. A variant worth shipping as files is one that adds something — content that would otherwise be lost, the way dev-pr.yml itself would be. A variant that only deletes has nothing to preserve, so it stays documented, exactly like Public vs private repos below.

ci.yml and codeql.yml list both main and dev in their on: branches: filters and neither edit touches them. A filter naming a branch that doesn't exist is a no-op, so it costs a main-only repo nothing — and without dev in ci.yml, PRs into dev (Dependabot's included) would run no CI at all.

Variants that are purely deletions — see Public vs private repos below — stay documented rather than vendored; only this one earns the folder.

Public vs private repos

Some meta defaults only make sense for one visibility. When spinning up a repo from this template, adjust for its visibility:

  • CodeQL / code scanning (.github/workflows/codeql.yml) depends on GitHub Advanced Security. It's free on public repos; on a private repo without a GHAS license it won't run — delete codeql.yml (and the CodeQL note above) rather than leave a dead workflow. The same goes for other GHAS-gated features (secret scanning, etc.). Dependabot version updates work on both.
  • License. A public repo ships MIT: keep LICENSE and the [MIT](LICENSE) © … README footer. A private repo is proprietary: remove/replace LICENSE, drop the MIT footer, and set package.json to "license": "UNLICENSED" (keep "private": true).
  • Discord forum links. .github/ISSUE_TEMPLATE/config.yml points questions, ideas and possible bugs at the repo's Discord forum (each open-source repo gets one, provisioned from the infrastructure repo's OpenTofu). Confirmed bugs and features stay as the GitHub issue forms. A private repo has no forum — drop the contact_links block; if you still want an in-repo Q&A path, restore a simple question.yml.

House style for READMEs and meta files

/write-readme skill encodes the canonical structure. Key rules: hero block wrapped in <div align="center">, prescribed section emojis (✨ Features, 🚀 Setup, 🤝 Contributing, 🛣️ Versioning, 📄 License), license footer always reads [MIT](LICENSE) © [Titus Kirch](https://github.com/TitusKirch/) / [IT-Dienstleistungen Titus Kirch](https://kirch.dev). Use GitHub callouts (> [!TIP], > [!IMPORTANT]), never plain blockquotes.

When editing this template

  • Every file referencing TitusKirch/scaffold is a placeholder that downstream users will replace. Keep the references consistent so a single grep -rn "TitusKirch/scaffold" catches them all.
  • forgemap (sibling repo at ../forgemap) is the de-facto reference implementation of these conventions. When unsure about a config choice, check what forgemap does.
  • The template's own package.json is "private": true and "name": "scaffold" — not published anywhere.