Skip to content

sync-workspace: generate_exclude refuses config-driven carrier changes, so a vault.sync.include edit can be silently inert #2781

Description

@sonichi

generate_exclude cannot tell an operator's hand-edit from a config change, so it refuses both

scripts/sync-workspace.sh::generate_exclude compares .git/info/exclude against the
content it would generate. If they differ and --force-gitignore is absent it warns,
prints the diff, and returns 1:

sync-workspace: <path> EXISTS and DIFFERS from the generated content.
Refusing to overwrite (operator-authored content may block carrier-set paths).

The guard's stated purpose is protecting operator-authored content. But a config edit is
exactly what makes the two differ
, so the refusal fires on every legitimate
vault.sync.include change too. The two cases are indistinguishable to it.

Measured consequence

Adding talks/ to vault.sync.include on this host had no effect for ~20 minutes. The
exclude was never rewritten, talks/ stayed ignored, and the vault kept not backing it up.
Nothing surfaced it except the carrier-set health probe:

✗ carrier-set  1 configured carrier path(s) are STILL GIT-IGNORED so the vault is not
               backing them up (talks/) — the exclude file is stale and sync refused to
               regenerate it

Sync's own stdout for that tick was just sync-workspace: pushed to host/<host>/<wsid>.
The refusal goes to the sync log, which on a cron nobody reads. So the failure mode is: a
config edit reads as applied, does nothing, and the only signal is a probe the operator
may not run.

bash scripts/sync-workspace.sh --force-gitignore fixed it — after confirming all 46
exclude lines matched the generated structure, so nothing hand-authored was at risk.

Why this is worth fixing rather than documenting

The check is comparing the wrong two things. It asks "does the file differ from what I
would generate now?", which conflates:

  • the file was hand-edited (the case it wants to catch), and
  • the config changed since the file was generated (a case it should just apply)

Recording a digest of the content as generated — alongside the file, or as a comment
line in it — would separate them: file matches its recorded digest → regenerate freely;
file diverges from its digest → an operator touched it, refuse as today. The header
already declares # Generated by sync-workspace.sh — do not edit by hand, so a digest
line is consistent with what the file already claims about itself.

Peer-visible effect

A second host hit the same guard from the other side: it hand-merged !talks/ +
!talks/** rather than use --force-gitignore, and that only worked because what it typed
happened to be byte-identical to what the generator emits. Anything else would have marked
the file as operator-authored permanently, making every later carrier edit on that host
inert.

Reported by Sutando-Pro; measured independently here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions