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.
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 editedRetyping 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.
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.
| 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.
- Node 24, pnpm 11. Pinned via
.nvmrc,engines, andpackageManager.pnpm-workspace.yamlenforcesminimumReleaseAge=4320(3-day cooldown), isolated node-linker. Don't loosen these without reason. - oxc, not eslint/prettier. Linting via
oxlint, formatting viaoxfmt. Configs live in.oxlintrc.json/.oxfmtrc.json.oxlintusesunicorn+oxcplugins; rules deliberately minimal. - Husky hooks (
.husky/pre-commit,.husky/commit-msg) runlint-stagedandcommitlint.lint-staged.config.jsexcludesREADME.md,CLAUDE.md, andAGENTS.md(free-form prose) andpnpm-lock.yaml.oxlint --fix --deny-warningsthenoxfmton JS;oxfmtonly on JSON/YAML/MD. - Conventional Commits enforced via
@commitlint/config-conventional. Don't--no-verifyunless 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 usesrelease-type: simple(language-agnostic),include-v-in-tag: true. Downstream repos start at0.0.0and 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-typescriptwithsecurity-extended,security-and-qualityqueries, 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.
.claude/settings.jsonships a baseline permission policy — see Permission policy below for the rules it follows..claude/settings.local.json(per-machine overrides, typicallyenabledMcpjsonServers) is gitignored..tituskirch-skills.jsonconfigures the TitusKirch skills (commit, PR, issue, release, docs …) per repo. It is the runtime config, not an installer. Regenerate/reconcile it with thetituskirch-skills-configskill.- 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:updaterefreshes project-scoped skills tracked inskills-lock.json(only present once a repo actually installs project skills).
.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 --forceParity 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_rulematches 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 theDELIBERATElist in the script and in the.codex/rules/default.rulesheader — 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 /andrm -fr /are separate bans;rm -r -f /andredis-cli FlushAllare 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.
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 dev → main 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.
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 — deletecodeql.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
LICENSEand the[MIT](LICENSE) © …README footer. A private repo is proprietary: remove/replaceLICENSE, drop the MIT footer, and setpackage.jsonto"license": "UNLICENSED"(keep"private": true). - Discord forum links.
.github/ISSUE_TEMPLATE/config.ymlpoints questions, ideas and possible bugs at the repo's Discord forum (each open-source repo gets one, provisioned from theinfrastructurerepo's OpenTofu). Confirmed bugs and features stay as the GitHub issue forms. A private repo has no forum — drop thecontact_linksblock; if you still want an in-repo Q&A path, restore a simplequestion.yml.
/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.
- Every file referencing
TitusKirch/scaffoldis a placeholder that downstream users will replace. Keep the references consistent so a singlegrep -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.jsonis"private": trueand"name": "scaffold"— not published anywhere.