A Go CLI plus a set of profile templates that standardize how Claude Code works
across every project in ~/Developer. The lacquer renders shared content —
CLAUDE.md rules, skills, commands, CI workflows, git hooks, tool configs, the
credential rules in .gitignore — into
each project and tracks how far each project has drifted, so a lesson pinned once
propagates everywhere instead of being copy-pasted and left to rot.
A project opts in per component (a subdirectory) via a .lacquer.toml
manifest. Each component declares one or more profiles; core applies to
every project regardless.
| Command | Does |
|---|---|
lacquer init [--stack S] |
Detect components, write a .lacquer.toml stub (and a docs/brief.md stub). --stack also declares the components the project doesn't have yet. |
lacquer init --list-stacks |
Print the archetypes --stack accepts. |
lacquer onboard --org O [--no-repo] |
init, then create a private GitHub repo under O when the repo has no origin. |
lacquer adopt |
Re-detect, and record any stack that appeared since init into .lacquer.toml. Only ever adds. |
lacquer sync [--force] [--fix] |
Render core + per-profile content into the project (managed regions + whole-file assets); --fix then runs the autofixers. |
lacquer fix |
Run each profile's autofixers (formatter, lint --fix) over the project source. |
lacquer settings [--project P] [--target T] [--configuration C] [--json] [SETTING...] |
Inspect static build settings with provenance; --xcode queries Xcode, --xcode --compare compares both. |
lacquer ratchet [--write] |
Measure project ceilings; --write enrolls or tightens the committed baseline. |
lacquer doctor |
Prove each check can fail: feed known-bad input and assert it's rejected (exit 5 if one can't). |
lacquer skills |
Install [project].skills entries via the skills CLI. |
lacquer plugins |
Install core/bootstrap/plugins.toml (machine-level Claude Code plugins) via claude plugin. |
lacquer status |
Show each region's stamped version vs the lacquer's latest. |
lacquer audit |
Classify project drift; exit 3 if a sync would clobber a local change, 4 on a baseline violation, 6 on an undeclared stack (usable as a CI gate). |
lacquer wait pr <N> |
Block until every check on a PR is terminal (no tokens while it sleeps). Exit 0 none failed, 1 a check failed, 2 timed out, 3 no checks — an empty check list is never a pass. Exits 2, 3 and 4 also raise an inbox ACTION (--no-inbox to skip). The sanctioned way to wait for CI. |
lacquer version |
Print labeled content and build versions, plus the resolved content root path. |
lacquer --help prints usage.
lacquer settings --project App.xcodeproj --target App --configuration Release
lacquer settings --project App.xcodeproj --json MARKETING_VERSION
lacquer settings --project App.xcodeproj --target App --configuration Release --xcode --scheme App --compareStatic output is labelled static resolution by lacquer: not Xcode's evaluation; defaults and conditionals are not applied. It uses the baseline reader's layers:
project xcconfig, project pbxproj, target xcconfig, target pbxproj. Each setting
shows its winning layer/file, UNSET when no declaration exists, or UNKNOWN
with a reason when the reader cannot determine it. UNSET does not imply Xcode
has no default. An unreadable project or unmatched selection exits non-zero.
The default keys are SWIFT_VERSION, SWIFT_TREAT_WARNINGS_AS_ERRORS, and
SWIFT_STRICT_CONCURRENCY; positional keys replace that list. With no project,
exactly one .xcodeproj must exist in the current directory. Omitted target or
configuration selects all target configurations. Put flags before setting names.
--xcode requires an explicit --scheme S (missing it is a usage error).
It calls xcodebuild -showBuildSettings -json -project P -scheme S -configuration C
for each selected pair and filters the JSON by target; it does not build or infer
a target-to-scheme mapping. If the scheme does not build a selected target, the
command fails and lists the targets it does build. Each invocation supplies a
temporary -derivedDataPath,
-disableAutomaticPackageResolution, and -skipPackageUpdates, then removes
the temporary directory. Failures report stderr and exit non-zero without a
static fallback. Xcode output identifies its evaluator, but does not expose a
winning source file. --compare requires --xcode and marks MATCH, DIFF, or
UNKNOWN comparison; differences are informational, not an exit-code gate.
--json retains the label and explicit set/unset/unknown states.
lacquer console needs no flags and no files. It prints the inbox's open
ACTION/UNREAD entries and every live Claude Code session on the machine, read
from claude agents --json. If that cannot be read it says
sessions: unavailable — <reason>; an empty list is only ever printed when
claude answered with one. The inbox defaults to
$XDG_STATE_HOME/lacquer/inbox.jsonl (~/.local/state/lacquer/inbox.jsonl),
created on first write; --inbox and $LACQUER_INBOX override it. Add
--roster (or $LACQUER_ROSTER) for fleet truth, open PRs and the project each
session belongs to. --sessions is only for dispatch records, which
watch --relaunch and kill need.
A background agent that goes idle writes itself into the inbox: the iOS, web
and supabase profiles ship a Claude Code Stop hook (lacquer console inbox hook stop) that adds one
UNREAD, <session> is idle in <project>: <first line of its last message>, ref
session:<id>, only when $CLAUDE_JOB_DIR is set (a claude --bg session), and
at most one open entry per session. It always exits 0; problems go to stderr.
The iOS profile's .claude/settings.json carries the editor guards as well; web's
and supabase's carry only this hook, and marketing ships none. A project with
more than one of these profiles gets one file, composed by lacquer sync: each
hook event's matcher groups are unioned by matcher, identical hook entries
appear once (so the Stop hook fires once, not once per profile), and if two
profiles set the same top-level key other than hooks to different values the
sync fails naming both profiles and the key.
Decisions were lost when they were paraphrased on the way into a brief, and the
operator's answers lived in one file on one machine. lacquer decisions reads
the alternative: the operator's words, verbatim, in GitHub next to the work they
govern. The log is issue-backed. Nothing is committed and there is no rendered
file to go stale.
In the inbox detail popup, D records a decision: the operator's words, kept
exactly as typed (never trimmed, reflowed or summarised), an optional basis
(the measurement it was decided against, because decisions expire into facts),
then r for this repo or f for fleet-wide, with the target of each shown before
anything is posted. Esc at any prompt posts nothing. An ordinary r reply stays a
reply: it goes to the overseer and is never recorded, and a decision goes to
GitHub only.
A decision is one comment on a repository's one open issue labelled decisions.
This repo means the repository the item's ref names, else its project mapped
through the roster. Fleet-wide means the fleet repository ($LACQUER_FLEET_REPO,
default patrickserrano/fleet-ops), so a decision that spans repositories lives in
one issue and not in a copy per project. The first decision in a repository
creates the decisions label and an issue titled Decisions; two open
decisions issues are refused by name. Every write goes through the same gate as
a reply's comment, so a repository outside the roster and $LACQUER_EXTRA_REPOS
is refused, the fleet repository included, and each write happens only on the
operator's keypress. The comment holds nothing an agent wrote:
**Decision** — 2026-09-25T04:10:00Z · from https://github.com/o/r/issues/5
```text
<the operator's words, exactly as typed>
```
Basis:
```text
<the basis, if one was given>
```The words sit in a code fence rather than a quote, because a fence is the one form
that shows a string as written: in a quote #123 writes a cross-reference into
another issue and @name notifies someone.
lacquer decisions # this checkout's repository, from origin
lacquer decisions owner/name
lacquer decisions --fleet # the fleet repositoryIt prints them oldest first and only reads. None recorded prints
no decisions recorded for <repo> and exits 0; a gh that could not answer, or two
open decisions issues, exits 1, so empty and broken never look alike. The managed
CLAUDE.md/AGENTS.md region tells agents to run the first and the last before
briefing or starting work.
console dispatch starts ICs on sonnet by default. Set top-level
ic_model = "opus" and/or ic_effort = "low" in the fleet roster to change
fleet-wide defaults. console dispatch-role instead uses model/effort in
its [[role]] entry; absent settings inherit Claude's defaults.
Both commands accept --model M and --effort E, overriding each configured
value independently in bg and tmux modes. For example:
lacquer console --roster fleet.toml --sessions sessions.jsonl --mode bg dispatch app "implement the unit" --model opus --effort lowWith --sessions, JSONL records keep the requested model and effort, including
failed launches. The dashboard and console watch show those settings, and
watchdog relaunches retain explicit recorded values. Empty values (including
old records) display inherited/unknown; they do not imply Sonnet. These are
requested settings, not transcript verification of the service's actual
model. Verify that separately from the session's own transcript when auditing
model usage. Dry runs print the selected flags and write no session record.
Before an agent starts in a new, assigned, or resumed worktree, console writes
.metadata_never_index at its root to exclude build churn from Spotlight.
The marker is ignored through the user's global core.excludesFile, or Git's
default $XDG_CONFIG_HOME/git/ignore (~/.config/git/ignore without XDG).
Console preserves existing entries and appends the marker rule once; it never
adds it to project .gitignore files. An unwritable global file or a repository
ignore override that exposes the marker prevents launch with an error.
Dry runs write neither markers nor exclusions. Existing worktree branches and
source files are preserved. Console does not remove old worktrees.
Every command that reads shipped content resolves LACQUER_ROOT (default .)
and prints its absolute root path, tag or branch, and commit to stderr.
The root must be a clean Git checkout, detached at a tag matching VERSION.
Branches, dirty trees (including untracked files), missing Git, unknown commits,
and failed Git inspection are refused. Verification is local; it does not fetch
or claim that a pinned release is the newest release.
LACQUER_ROOT=~/.local/share/lacquer/content lacquer statusFor deliberate development against a working checkout, explicitly opt in:
LACQUER_ALLOW_UNVERIFIED_ROOT=1 LACQUER_ROOT="$PWD" go run ./cmd/lacquer statusEvery invocation that uses this override warns that its output is UNREVIEWED.
The separate LACQUER_ALLOW_STALE_BINARY override still governs sync when the
binary and content versions differ. Neither override proves a release pin.
status labels version-marker drift stamp-behind and checks whether managed
content still matches; audit classifies content drift. A release affecting only
another profile can leave a project's stamps behind while its content matches.
If the root has no VERSION file or profiles/ directory, commands fail with an
actionable message before trying to read content.
core— universal rules/skills/commands applied to every project.ios— Swift/Xcode: SwiftLint/SwiftFormat, CI, TestFlight, Skills; git hooks viapre-commit.web— TypeScript + Biome + Vitest; CI + git hooks vialefthook; Claude Stop hook (.claude/settings.json).supabase— Deno Edge Functions + Postgres/RLS; CI + git hooks vialefthook; Claude Stop hook (.claude/settings.json).marketing— no CI, no hooks, skills only: ~50 marketing/growth skills (ads, SEO, copywriting, funnels, lifecycle, pricing, planning). Never auto-detected — there is no marketing "stack" on disk to find, so add it to a component'sprofilesdeliberately when marketing work is in scope.
A component detected as an unshipped stack (e.g. Rust/Go, or a bare SwiftPM
package) is recorded in the manifest with an empty profile list and a notice —
it doesn't break sync, and audit keeps reporting it so the gap stays
visible.
Detection can only see what is already on disk, which makes it useless at the
one moment the stack is actually being decided — while the idea is still a brief
and a PCD. So name the stack there, as an archetype, and hand it to init:
lacquer init --list-stacks
lacquer init --stack ios-supabase--stack declares the components a project of that kind has, including the ones
that do not exist yet, so both halves are gated from the first commit. Detected
components always win where the two disagree — the archetype only fills gaps.
See archetypes/.
Projects that grow a stack after onboarding are the other half of the problem.
sync and audit now re-run detection every time, and:
- a stack the lacquer ships a profile for blocks — run
lacquer adoptto record it, or add the path to[project].excludeto keep it unmanaged; - a stack no profile covers is reported on every run and gates nothing — that gap is the lacquer's, not the project's.
Detection used to run exactly once, at init, and never again. One repo
bootstrapped as TypeScript-only during a spike, grew a Swift package the next
day, and a year later still declared profiles = ["web"]: no hooks, no CI, 191
tests run by nothing at any gate. Another declared its iOS app but not the
Supabase backend or the admin web app sitting beside it. Neither ever produced
an error, because nothing ever asked.
lacquer audit # see what drifted; exit 3 means sync would overwrite a local edit
lacquer sync # apply; refuses to clobber a locally-modified managed unit
lacquer sync --force # adopt the lacquer version over a local changefleet uses the same blocking policy as audit, including still-present
orphans, and names those orphans in its report. The uncalled-script report
recognizes scripts named in rendered CLAUDE.md instructions as agent entry
points (including their script helpers). Removing that documentation exposes an
otherwise uncalled script again; other Markdown files do not grant exemptions.
This checks wiring or documented use, not proof that a script executed.
Sync writes a .lacquer.lock baseline so audit can tell "the project edited
this" from "the lacquer moved on". With an existing lock, sync also refuses
untracked-conflict units: lacquer now ships a path (or managed region) where
the project already has differing content, but no lock entry records ownership.
Review it, then use --force to take lacquer's content or exclude/disown the
unit. First sync, with no lock at all, still adopts existing content and prints
which units it replaced. Identical content is accepted without a clobber warning;
uncommitted asset changes remain protected even with --force.
After syncing a project, run lacquer ratchet --write and commit
.lacquer.ratchet.toml to enroll it. The separate project-owned TOML file keeps
metric decisions out of .lacquer.lock, which sync regenerates as content hashes:
[ratchet]
claude_md_project_lines = 120
unjustified_suppressions = 7lacquer ratchet measures without writing. audit and fleet compare enrolled
projects against their ceilings. A regression prints both numbers and gates with
exit 4 (audit's destructive-drift exit 3 still takes precedence). An improvement
prints ratchet: METRIC improved A → B. sync and ratchet --write save lower
values automatically; neither raises an existing ceiling. Sync completes its
normal rendering before measuring, and returns 4 if a metric still regressed.
Without a baseline, audit reports how to enroll; it does not invent a ceiling.
To accept a specific increase, run:
lacquer ratchet --loosen unjustified_suppressions --reason "Legacy migration requires this suppression"This records the current value and a per-metric string in [reasons].
Commit that diff for review. A historical reason never exempts later increases.
Malformed baselines, unknown metrics, and empty loosening reasons are errors.
Metric definitions:
claude_md_project_lines: physical lines outside Lacquer managed-region markers in current root and profiled-componentCLAUDE.mdfiles, counting each destination once. Managed bodies and marker lines are excluded; blank lines outside regions count.AGENTS.mdmirrors are not counted. Malformed region boundaries fail measurement. A Lacquer content increase cannot consume the project prose budget. This measures files on disk, not future rendering.unjustified_suppressions: directive comments in tracked Swift, JavaScript, TypeScript, Vue, Svelte, CSS, and JSONC source. Countsswiftlint:disable,biome-ignore, andeslint-disablevariants without a trailing justification. Biome uses:, ESLint uses--, and SwiftLint accepts//, dash, or colon separators. Empty punctuation is not a reason. Enable/end directives and quoted examples are excluded. Git symlink entries and submodules are not source blobs; tracked regular targets are counted once. Untracked files are excluded.
Lacquer owns a separate CI ceiling in
internal/shipped/claude_ratchet_test.go: the existing rootapp, multistack,
duoapp, and spmpackage fixtures are rendered and their managed lines (including
markers) must stay at or below pinned constants. Lower those constants in the
same PR whenever the managed text shrinks, including work on #453.
Measurement errors fail closed. Stage source deletions before tightening; a missing tracked file is an error, not an improvement. Doctor exercises both metrics through the real audit path in a scratch project: establish a baseline, improve, sync, pass a clean audit, then prove each regression is rejected.
audit and fleet report build-setting names present in a tracked .xcodeproj
but absent after xcodegen generate, and the reverse, by target/configuration
and project/configuration. A sibling project.yml opts the project into this
comparison. It compares the current checkout, including local edits, in a scratch
copy; it does not replace the original project. Value-only changes and effective
xcconfig values are outside this presence check (the baseline checker still
resolves its own settings).
XcodeGen and macOS plutil are required. Missing tools, unreadable input, failed
generation, and unsupported inputs such as symlinks or generation hooks report NOT CHECKED; they
never stand in for a clean comparison. Findings are report-only. The iOS Mac lint
job runs lacquer audit --xcodegen-only before doctor, using the same pinned
release, so CI can report this even though the Linux drift job lacks XcodeGen.
A [baseline.relax] entry whose baseline passes in every checked component is
reported as a dead relaxation to remove, including strict concurrency implied
by Swift 6. Like stale exclusions, this notice does not change the exit code.
An unknown baseline or a key enforced only by CI (documentation, pgtap) is
reported as relaxation NOT CHECKED, never assumed live or dead.
Every serious defect found onboarding this fleet was the same shape: a check that ran, reported success, and verified nothing.
- The editor hook called
swiftlint lint --path FILE.--pathhad been removed from SwiftLint, so it errored on every write — and2>/dev/null || trueate the message. It linted nothing for months and looked healthy doing it. - The DocC gate passed
DOCC_FLAGS=--warnings-as-errors. That is a real build setting name and it is silently ignored; a deliberately broken symbol link still exited 0. - The drift job ran
go build ./pathfrom a non-module directory. It failed in every repo, on every PR, for a reason unrelated to drift.
None was caught by running the check. CI already tells you whether a check passes; it cannot tell you whether it could fail.
lacquer doctorwrites a known-bad fixture, runs the check against it, and asserts the check
rejects it. A probe that passes on broken input is reported as broken, with
the reason it exists. Exit 5 — distinct from audit's 3 (drift) and 4
(baseline), so a caller can tell "a check is broken" from "the project is
wrong".
proving each check can fail:
ok SwiftLint rejects a warning-severity violation under --strict
ok SwiftLint is invoked in a form it still accepts
ok SwiftFormat --lint rejects unformatted code
ok missing_docs rejects an undocumented declaration
ok the formatter and the linter agree on member order
5/5 checks proved they can fail.
Probes live in profiles/<p>/doctor.toml, and fixtures are written to a scratch
directory outside the project — a deliberately malformed file must never be
committable. A missing tool is a finding, not a skip: a check whose binary is
absent is not running. (The opposite of lacquer fix, where a missing tool
skips — an unfixed file is still caught by CI, an unverified one is not.)
lacquer sync writes the configs; it does not touch your source. On a mature
app that means the newly-synced .swiftlint.yml finds everything at once — 509
violations in one app here, 290 in another — and roughly two thirds of that is
mechanical: member ordering, import sorting, trailing closures.
--fix pays that down before you ever look at it:
lacquer sync --fix # sync, then run the profiles' autofixers
lacquer fix # just the autofixers, any timeMeasured on one app: 290 violations → 93, with the whole type_contents_order
category (117) going to 1, because the synced .swiftformat enables
organizeDeclarations in type mode with a --type-order mirroring
.swiftlint.yml. What's left is judgement work — singletons, closure length,
layering — which is the right thing to be left with.
--fix is opt-in on purpose. Plain sync only ever writes lacquer-managed
files, and that contract is what makes lacquer audit able to say "you changed
this, the lacquer didn't". --fix deliberately breaks it by rewriting project
source, so it has to be asked for rather than discovered in a diff.
A fixer whose tool isn't installed is reported and skipped, never fatal — the opposite of a check, which must block when it can't run. An unfixed file is still caught by CI; a missing Homebrew formula shouldn't block adoption.
lacquer sync distributes this repo's own skills (core/skills/,
profiles/*/skills/) — that's a solved problem, versioned and drift-audited.
Third-party skills are a different concern: one global install shared across
every project, kept up to date by vercel-labs/skills,
a real package manager for agent skills — not something lacquer reimplements.
The packages this fleet actually pulls in (with source links, and which is
suggested from which Swift import) are cataloged in Skills
reference —
currently dpearson2699/swift-ios-skills
(Apple framework references) and HunterHillegas/mac-assed-mac-app-skill
(AppKit/macOS conventions).
[project].skills in .lacquer.toml declares which packages this project
needs, mixing lacquer's own skills and third-party ones uniformly:
skills = [
"patrickserrano/lacquer@security-review",
"dpearson2699/swift-ios-skills@healthkit",
"dpearson2699/swift-ios-skills@storekit",
]lacquer init seeds this list automatically by scanning the project's actual
Swift imports (see internal/skillsuggest) — review and trim before running
lacquer skills, which installs exactly what's declared, project-scoped, via
npx skills add <source> -s <name> -p -y. Before invoking the installer, lacquer
refuses entries whose destination contains tracked files (including clean files
and local deletions), preserving project-owned skills while continuing with other
entries. It also flags any installed skill no longer declared in the manifest
(informational — nothing is auto-removed).
sync reads skills-lock.json offline and only reminds you about declared skills
missing from that record. An unreadable or malformed lock file produces a warning.
This is deliberately a separate command from sync: sync stays fully
offline and deterministic (its whole test suite depends on that), while
skills is the one command that reaches the network.
Claude Code plugins install once at user scope and are shared across every
project on a machine — a different shape of problem than [project].skills,
which is per-project. core/bootstrap/plugins.toml lists the marketplaces and
six plugins this fleet relies on — superpowers, codex (adversarial review
via a real Codex subprocess), context7, figma, security-guidance, and
telemetrydeck-analytics — cataloged with source links in Plugins
catalog.
lacquer plugins applies the manifest via claude plugin marketplace add /
claude plugin install, both confirmed idempotent (an already-configured
marketplace or already-installed plugin is a clean no-op). Only plugins
actually enabled on the reference machine are listed — one
installed-but-disabled there is a deliberate choice, not silently re-enabled
on a fresh machine.
lacquer pluginsThis is how a fresh machine picks up the same plugin set an existing one
already has, closing the same "bootstrap a machine with none of this
preconfigured" gap that [project].skills closes for per-project skills.
go install github.com/patrickserrano/lacquer/cmd/lacquer@latestOr build from a checkout:
go build ./cmd/lacquerTagged releases (with prebuilt darwin/amd64 and darwin/arm64 binaries and
changelogs) are published automatically on GitHub
Releases whenever
VERSION changes on main.
VERSION is semver and machine-assigned — never edit it in a PR. CI rejects
that, because two open PRs both bumping to the same number merge cleanly (both
sides make the identical change) and produce two different contents sharing one
version. Hand-picking also produced a permanent gap: v0.69.0 does not exist,
because two bumps were missed and had to be corrected at once.
Every merge with a material change releases. version.yml
derives the next version from conventional commits with
svu (pinned), writes VERSION, pushes, and
dispatches the release. release.yml tags v<VERSION> verbatim, so the file and
the tag cannot drift. There is no path filter — VERSION versions the whole
thing, CLI binary as much as synced content — and no loop, because a push made
with GITHUB_TOKEN does not trigger workflows.
| Commit | Bump |
|---|---|
feat: |
minor |
fix: |
patch |
feat!: / BREAKING CHANGE: |
major |
docs:, chore:, ci:, refactor:, style:, test: |
no release |
A README or docs-site edit ships nothing to a project, so it mints no version.
But prose under core/ or profiles/ is material — projects consume it, so it
must release, and docs: would leave lacquer status telling them they're current
while the content moved. Commit those as feat:/fix: even though they read like
docs. CI enforces this rather than leaving it to discipline: a PR touching
core/** or profiles/** with a non-releasing type is rejected.
Because the repo squash-merges, the PR title becomes the commit subject on
main — so it is the PR title that CI validates, and the version is derived
from. A non-conventional title would otherwise compute no bump and silently
release nothing.
The PR body is load-bearing for the same reason, with a sharper edge: svu
decides "breaking" by substring-searching the commit body, not by parsing a
conventional footer — and that is the only check that reads the body at all
(feat:/fix: are matched against the subject alone). So quoting the marker
anywhere, even inside a code span or a table cell, declares a breaking change.
#89 proposed 1.0.0 off a 0.72.0 base because its description reproduced the
table above. CI now rejects a body carrying that marker unless the title also
declares it with !.
The project sits in 0.x until a breaking change lands. feat!: /
BREAKING CHANGE: is the standardized, explicit way to declare one, so it
graduates the major — 0.x → 1.0.0, and 1.x → 2.0.0 after that. If you need
to set a version by hand for any other reason, push it to main directly; the
workflow leaves a hand-set value alone.
The version is stamped into each managed region's marker so lacquer status can
report stamped-vs-latest. A project last synced before semver carries the old
integer form (v70); that reads as 0.70.0 and is re-stamped on its next sync.
Projects enabling codex or antigravity receive dedicated AGENTS.md regions
from core/AGENTS.core.md and profiles/*/AGENTS.*.md; these retain the safety
rules and concrete checks without copying tool-specific procedures. Tests cap
rendered core plus profile instructions at 10,000 bytes. Project-owned prose
survives updates; existing locks migrate through the same region keys.
CLAUDE.md sources and rendering are unchanged.
Codex projects also receive .codex/hooks.json, a Python guard and an activation
README. Project and hook trust review is required; synchronization alone does
not enable enforcement. See Codex instructions for
coverage, limitations and regression commands.
A region is a block the lacquer owns inside a file the project also owns. Everything outside the markers is left untouched, which is what lets a project keep its own content in the same file.
| File | Marker key | Comment form |
|---|---|---|
CLAUDE.md, AGENTS.md (root) |
core |
<!-- ... --> |
<component>/CLAUDE.md, <component>/AGENTS.md |
the profile name | <!-- ... --> |
.gitignore (root) |
gitignore |
# ... |
.gitattributes (root) |
gitattributes |
# ... |
The .gitignore region carries the ignore rules that must not be a per-project
decision: App Store Connect keys (*.p8), signing material, Secrets.xcconfig,
.env and friends — with the committed templates (Secrets.xcconfig.example,
.env.example, .env.schema) re-included. It also names the third-party skill
trees installed from [project].skills, one by one, so the skills the lacquer
syncs into the same directories stay tracked and auditable. skills-lock.json
is ignored as local installation state.
Everything else in a project's .gitignore — DerivedData/, build outputs,
per-project junk — stays project-owned and survives every sync.
The .gitattributes region marks the agent-skill directories
(.claude/skills, .codex/skills, .agents/skills, whichever
[project].tools enables) as linguist-vendored. The lacquer ships ~207KB of
Python skill tooling into each of them, so a Swift repo taking the ios profile
picks up half a megabyte of code it did not write — and GitHub counted every
byte of it toward the language bar. One fleet repo read as more Python than
Swift. linguist-vendored suppresses the stats and collapses those paths in a
diff without untracking them, so lacquer audit can still see drift in a synced
skill.
This too is a region rather than a file, and for a sharper reason than
.gitignore: three fleet repos already keep real content in .gitattributes —
Git LFS filters, line-ending normalization, their own linguist overrides.
Shipping a whole file would have broken LFS.
Sync manages a runner-label region in .github/actionlint.yaml, using the
same custom-label list tested against the rendered workflows. Existing labels,
paths, config-variables, and comments survive. First adoption may normalize existing YAML formatting to create the block
sequence; subsequent syncs replace only the marked region. audit, status, and the lock track that region.
Web projects can keep generated files out of Biome without excluding the shared
biome.json. Declare extra negated files.includes patterns in .lacquer.toml:
[web]
biome_ignores = ["!**/payload-types.ts"]Patterns apply relative to each web component's biome.json. With no entries,
the rendered config is byte-identical to the default. The file remains wholly
managed, so future shared lint improvements still arrive. Doctor checks the
synced runner labels and declared ignores, with negative controls that reject
missing values; these configuration checks need no external linter binary.
The opt-in rule eval suite compares Claude's behavior with
and without rendered core plus each shipped profile’s CLAUDE.md instructions
and six skill routes. Its offline regression tests run
with go test ./...; paid model runs require an explicit local invocation.
docs/plans/ holds the design and build plans. The design doc
(docs/plans/2026-06-15-lacquer-design.md) carries an "Implementation status"
note distinguishing what's built from what's still aspirational.
Built by Patrick Serrano, an iOS engineer building apps under PixelFox Studio. lacquer is the internal tooling that keeps engineering practice consistent across the whole fleet.