--- title: "Grav plugin won't enable because its code is missing while its config persists in the env tree" date: 2026-07-05 category: integration-issues module: git-sync problem_type: integration_issue component: tooling severity: high symptoms: - "git-sync plugin will not enable on prod despite enabled: true in its config" - "Plugin does not appear / cannot be toggled on in the Grav Admin UI" - "Config-level fix attempts (editing plugin YAML) have no effect" - "make remote-gpm-install-prod PKG=git-sync reports a FRESH install, not 'already installed'" root_cause: incomplete_setup resolution_type: dependency_update related_components: - documentation - development_workflow tags: - grav - git-sync - gpm - plugin-management - env-config - config-without-code - troubleshooting-order - remote-only-plugin --- # Grav plugin won't enable because its code is missing while its config persists in the env tree ## Problem After remediating an unrelated git-sync secret leak on prod (intotheeast.com, Grav 2.0.7 on a DirectAdmin/Apache shared host), the operator went to re-enable the `git-sync` plugin. Setting `enabled: true` in its config had no effect: the plugin would not appear as enabled in the Grav Admin plugins UI, and toggling it on manually in the browser did not take either. It looked fully "configured" — the config file was right there — but the plugin was inert and sync never ran. The trap is that the plugin's config file existed (in the per-host env tree at `user/env/intotheeast.com/config/plugins/git-sync.yaml`), so any inspection that reads only config concluded the plugin was present and just needed enabling. The actual problem was one layer down: the plugin's **code** was missing from `user/plugins/git-sync/`. A Grav plugin cannot load or enable without its code on disk, no matter what its config says. ## Symptoms - The git-sync plugin does not appear as enabled, and cannot be enabled, in the Grav Admin UI — despite `enabled: true` being present in its config. - Toggling `enabled` in config, or flipping the toggle in the Admin UI, produces no working plugin. Sync does not run. - The plugin's config file DOES exist (in the per-environment tree `user/env/intotheeast.com/config/plugins/git-sync.yaml`), so the plugin appears "present" whenever only the config is inspected — masking the real state. ## What Didn't Work 1. **Setting / ensuring `enabled: true` in the git-sync config.** No effect. Config was never the problem. 2. **Enabling the plugin manually in the Admin UI.** The toggle wouldn't take. Both failed attempts operate on the **config** layer. But the plugin's **code** was absent from `user/plugins/git-sync/`, and Grav can't load a plugin without its code. Grav (and any tooling that reads the config tree) reports a plugin as "configured" purely from the presence of its config file, which masks the absence of code. Diagnosing and poking at the config layer could never fix a missing-code problem — and guessing at config changes before running a simple `ls` on the plugin directory cost real time here. ## Solution First, run the decisive diagnostic on the server — confirm whether the plugin code actually exists *before* touching config: ``` ls -la $WEBROOT/user/plugins/git-sync/ # empty/absent => missing code, reinstall ``` (In this project, do that via a make target or an ssh one-liner the user runs — never raw SSH by the assistant. All server ops go through `make remote-*`.) With the directory confirmed empty/absent, reinstall the plugin's code via GPM: ``` make remote-gpm-install-prod PKG=git-sync # GPM fresh-installs Git Sync v3.4.4 ``` That make target runs, on the server: ``` php bin/gpm index -f && php bin/gpm install git-sync -y && php bin/grav clearcache ``` The install output read **"Preparing to install Git Sync [v3.4.4] ... Success!"** — a **fresh** install, not "already installed." That fresh-install line is exactly what confirmed the code had been absent all along. After the reinstall plus cache clear, the plugin enabled and sync worked. ## Why This Works Grav resolves a plugin from **two independent locations**: - **Code** at `user/plugins//` — installed by GPM. Note `user/plugins/` is gitignored (`/plugins/*`) and is NOT tracked by the content repo. - **Config** — the tracked `user/config/plugins/.yaml` and/or the per-host `user/env//config/plugins/.yaml`. These two can **desync**: config can exist with no code behind it. Config alone makes the plugin look present to any tool that only reads config, but the plugin stays inert until its code is on disk. GPM reinstall restores the code; `clearcache` makes Grav re-scan and pick it up. A project-specific amplifier made this worse: `git-sync` is a **remote-only, GPM-managed** plugin. It is deliberately NOT in `plugins.txt`, so `make install-plugins` and the normal `make remote-install` flow do **not** restore it. Only an explicit `php bin/gpm install git-sync` (via `make remote-gpm-install-prod PKG=git-sync`) does. So when its code goes missing, it does not self-heal through the standard install path — you must reinstall it explicitly. How the code went missing here is **unconfirmed**. It happened around the git-sync secret-leak remediation, but the exact step that wiped `user/plugins/git-sync/` was not established — don't assume a specific cause. ## Prevention - **Check the code layer before the config layer.** When a Grav plugin "won't enable" and config toggles do nothing, FIRST verify the code exists: `ls user/plugins//` on the server. Config-without-code is the failure class; the empty directory is the tell. - **Enumerate both layers in all locations when diagnosing.** Plugins have a code layer (`user/plugins//`) and a config layer, and on prod the config can live in the env tree (`user/env//config/plugins/.yaml`) and persist completely independently of the code. Remember: once `user/env//` exists, Grav Admin writes ALL config there, so always check both `user/config/...` and the env path (env wins). - **Know which plugins are remote-only.** The 3-category model: GPM-via-`plugins.txt` (admin2 / api / flex-objects), custom-in-repo (cache-on-save / story-blocks), and remote-only (git-sync — never in `plugins.txt`). Remote-only plugins are NOT restored by the standard install/content flows, so reinstall them explicitly via GPM after any operation that could have wiped `user/plugins/`. - **Diagnose actual state before proposing config fixes.** An `ls` is cheaper than a guess. Establishing that the code was missing would have pointed straight at the reinstall instead of a round of config poking. ## Related - `docs/solutions/integration-issues/stale-grav-version-blocks-api-plugin-install.md` — **closest sibling.** Same family (a plugin non-functional on prod, resolved by a GPM install + cache clear), same 2026-07-04/05 cutover context, same `plugins.txt` / `make remote-*` / GPM machinery. **Distinct trigger:** there, GPM refuses to *offer* the plugin because the installed core is below the version floor; here, the plugin's *code folder is simply missing* while its config persists (config-without-code desync). Two different ways a plugin ends up absent/inert on prod. - `docs/solutions/architecture-patterns/git-sync-secret-exposure-and-tracked-file-boomerang.md` — same module (git-sync) and explains **why the config survived without code**: Grav Admin writes `git-sync.yaml` into the per-environment tree `user/env//config/plugins/`, which is untracked/gitignored and not part of the plugin package. The orphaned config here is the flip side of that env-tree behavior. - `docs/solutions/conventions/grav-plugin-config-must-be-tracked-override.md` — establishes the plugin **code (GPM/gitignored) vs config (tracked override / env tree)** split that this bug exploits. This doc is a concrete failure of that split going the other way: config present (in the env tree), code absent. - `docs/working/git-sync-notes.md` — operational notes on git-sync's per-environment tree, where `git-sync.yaml` lives server-only. Context for where the orphaned config resided.