The prior model claimed git-sync's add-set is scoped to the configured `folders` (pages/config/themes), so accounts/ and user/env/ were "safe by construction." That was wrong: prod auto-commit 9337003 pushed the whole user/env/<host>/config tree (JWT secret, CSRF salt, git-sync token + webhook secret), accounts/mischa.yaml, and system.yaml to Gitea — all outside the configured folders. git-sync stages everything under user/ not gitignored; .gitignore is the only reliable exclusion. - Rewrite the architecture-patterns doc around the corrected predicate and document the incident + remediation (gitignore /env/, rotate token+webhook). - Correct git-sync-notes.md: env/ is NOT safe on folder scope; drop the "never reaches Gitea" claim; fix the secret-files table intro + add /env/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
6.0 KiB
title, date, category, module, problem_type, component, severity, applies_when, related_components, tags
| title | date | category | module | problem_type | component | severity | applies_when | related_components | tags | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Grav plugin config must live in the tracked user/config/plugins/ override, not the plugin folder | 2026-07-04 | docs/solutions/conventions | grav / plugin configuration | convention | tooling | high |
|
|
|
Grav plugin config must live in the tracked user/config/plugins/ override, not the plugin folder
Context
Grav resolves a plugin's config by deep-merging two layers: the plugin's own shipped file user/plugins/<name>/<name>.yaml (installed by GPM, part of the package) and the tracked override user/config/plugins/<name>.yaml (which wins). In this project the content repo tracks only pages/, config/, accounts/, themes/; user/plugins/ and user/data/ are gitignored (GPM manages plugin code). So any functional config a developer edits into a plugin's own user/plugins/<name>/<name>.yaml is invisible to version control.
It was this gap that left the api plugin unconfigured on the fresh prod install. Its enabled/route/session_enabled/cors/rate_limit config existed only in the untracked plugin folder locally, while the committed user/config/plugins/api.yaml held only a runtime popularity.salt. The local machine worked because the plugin folder had been hand-edited; every fresh environment got only the plugin's shipped defaults.
Guidance
Put functional plugin configuration in the TRACKED override user/config/plugins/<name>.yaml. Grav deep-merges it over the plugin's shipped defaults, so it need only carry the keys that must differ (or the full config, for clarity). Keep secrets and per-install generated values OUT of the tracked file — JWT secrets, salts, encrypted tokens belong in gitignored *-private.php companion files (e.g. api-private.php, security-private.php) or should be regenerated per-install.
Never rely on edits to the plugin's own user/plugins/<name>/<name>.yaml: it is gitignored (won't deploy) and is overwritten on the next php bin/gpm update.
Concrete before/after, using the api plugin:
# WRONG: user/plugins/api/api.yaml (gitignored, GPM-managed, wiped on update)
enabled: true
route: /api
auth:
session_enabled: true
# RIGHT: user/config/plugins/api.yaml (tracked, deploys, survives gpm update)
enabled: true
route: /api
version_prefix: v1
auth:
session_enabled: true
# JWT secret intentionally NOT here — it lives in the gitignored api-private.php
Why This Matters
Reproducible deploys: a fresh clone or make remote-install-<env> must produce a working site from the repo alone. Config stranded in the gitignored plugin folder silently yields a plugin with only its shipped defaults on every new environment — which, for a plugin whose behavior depends on non-default config, means it's misconfigured or effectively off. On prod the api plugin's route/auth simply didn't work.
The failure is silent and per-environment: it works on the developer's machine (where the plugin folder was hand-edited) and breaks everywhere else. gpm update compounds it by wiping the folder edit even locally, so the "working" state is not just unshared — it is also unstable on the one machine that had it.
When to Apply
- Any time you configure a Grav plugin whose non-default settings must work on a server (prod/test) or survive a plugin update.
- Especially for plugins whose function depends on config:
api(route/auth/cors),admin2,flex-objects, form/media settings, etc. - When auditing a fresh-install failure: check whether the "working" local config actually lives in a tracked path (
git ls-files user/config/plugins/<name>.yaml) or was stranded inuser/plugins/<name>/.
Examples
- api plugin (this project): the functional config was moved from the untracked
user/plugins/api/api.yamlinto the trackeduser/config/plugins/api.yaml, thenmake content-push+make remote-fetch-content-<env>deployed it. The JWT secret stayed in the gitignoredapi-private.php. - Quick audit command:
git -C user ls-files config/plugins/shows exactly which plugin configs are tracked/deployable; anything you rely on that isn't listed is a latent fresh-install failure.
Related
docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md— the config gap documented here was the other latent problem surfaced in that same investigation: theapiplugin also had to be installed first before any config could take effect. The install gap (GPM version floor) and this config-tracking gap compounded each other on the fresh prod environment.docs/working/git-sync-notes.md— the related third config location: on prod, Grav Admin saves config into the per-environment treeuser/env/<host>/config/, which is also untracked. Same "config that doesn't reach the repo" family.docs/solutions/architecture-patterns/git-sync-secret-exposure-and-tracked-file-boomerang.md— the sync-boomerang consequence of this rule: a per-install value that a plugin regenerates into a tracked functional config file (e.g.popularity.saltinapi.yaml) re-commits itself and ping-pongs across environments under bidirectional git-sync. The*-private.phpcompanion pattern this doc establishes is exactly the durable fix.- CLAUDE.md §0 (plugin-management model): only
pages/,config/,accounts/,themes/are tracked in theuser/repo;plugins/anddata/are gitignored and GPM-managed. That tracking boundary is exactly why functional config must live underconfig/plugins/, not in the plugin's own folder.