Files
intotheeast-com/docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md
T
m038andClaude Opus 4.8 6dc6af6359 docs: correct git-sync secret-exposure model (gitignore is the boundary, not folder scope)
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
2026-07-05 10:19:54 +02:00

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
Editing functional config for any GPM-managed Grav plugin
user/plugins/ is gitignored and only pages/config/accounts/themes are tracked
Preparing a fresh install or production cutover
A plugin behaves correctly locally but ships with only default config on deploy
grav
gpm
api plugin
content repo
deployment
grav
plugin-config
gpm
config-override
gitignore
deployment
api-plugin

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 in user/plugins/<name>/.

Examples

  • api plugin (this project): the functional config was moved from the untracked user/plugins/api/api.yaml into the tracked user/config/plugins/api.yaml, then make content-push + make remote-fetch-content-<env> deployed it. The JWT secret stayed in the gitignored api-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.
  • 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: the api plugin 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 tree user/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.salt in api.yaml) re-commits itself and ping-pongs across environments under bidirectional git-sync. The *-private.php companion pattern this doc establishes is exactly the durable fix.
  • CLAUDE.md §0 (plugin-management model): only pages/, config/, accounts/, themes/ are tracked in the user/ repo; plugins/ and data/ are gitignored and GPM-managed. That tracking boundary is exactly why functional config must live under config/plugins/, not in the plugin's own folder.