Skip to content

chore: keep agent workspaces out of the tree and make the ignore rules symlink-safe - #2

Merged
andrewschristison merged 1 commit into
mainfrom
gitignore-symlink-safe
Aug 6, 2026
Merged

chore: keep agent workspaces out of the tree and make the ignore rules symlink-safe#2
andrewschristison merged 1 commit into
mainfrom
gitignore-symlink-safe

Conversation

@andrewschristison

@andrewschristison andrewschristison commented Aug 6, 2026

Copy link
Copy Markdown
Member

What this changes

.gitignore only. No source, no data, no site content.

Two things, both about the same failure mode.

1. Agent workspaces no longer sit in the working tree. Their contents now
live outside any repository and are reached through symlinks. Previously the
only thing keeping them out of this public repository was an ignore rule. Now
the bytes are not in the tree at all, and a forced add would commit a path
rather than the content behind it.

2. The ignore entries lost their trailing slashes. .agents/ matches a
directory only. The moment that directory became a symlink, git saw a file, the
rule stopped matching, and the path fell out of the ignore set silently.
git add -A would have staged it. Slashless entries match a directory, a file,
or a symlink, so the rule survives the relocation.

The second point was found by running git check-ignore after the change
rather than reasoning about it beforehand. The first version of this work
introduced exactly the regression it was meant to prevent.

Verification

  • git ls-tree -r origin/main: 0 paths under these entries, before and after.
  • git log --all -- <paths>: 0 commits have ever touched them. History is clean
    and this change does not alter that.
  • git check-ignore -v: all three entries match explicit rules after the
    relocation.
  • git add -A --dry-run: stages nothing under any of them.

Does not cover

  • Git history, which is already clean here and is not modified.
  • The relocated files themselves, which are unchanged and simply live elsewhere.
  • Other repositories with the same trailing-slash pattern. Reviewed separately.

CI status at merge

CI NOT MEASURED. No run exists on this head (ec950dcc). The cause is
neither of the two candidates that were considered, and is stated here as
UNKNOWN rather than guessed.

Ruled out: workflow triggers not matching the branch. .github/workflows/ci.yml
declares pull_request: branches: [main], this PR's base is main, and the
workflow file is present and byte-identical on the head ref (a286dd27). There
is no paths filter and no job-level condition. The triggers match.

Ruled out: the GitHub Actions outage, as a sufficient explanation. The
pull_request event was deliberately re-fired by closing and reopening this PR
at 21:29Z, which is after Actions had demonstrably recovered: a sibling
repository executed a full job on a real runner at 20:47Z the same day. Twenty
nine minutes after the re-fire, no run exists. An outage that had recovered
cannot explain a re-fired event producing nothing.

Remaining candidate, UNKNOWN. Repository-level Actions settings are
permissive (enabled: true, allowed_actions: all). The organization-level
Actions policy could not be read: HTTP 403, requires the admin:org scope. An
org policy is the leading explanation, and it is recorded as unknown rather than
asserted, because it was not observed.

Baseline note, so an absence is not overread. This is the first pull request
ever opened in this repository. Every one of the 19 workflow runs in its history
is a push event. The absence of prior pull_request runs therefore carries no
information about whether they would fire, and is not offered as evidence.

Merging on that basis: main is unprotected with no required status checks,
verified through the branches API rather than assumed, so no check is being
overridden. The diff is .gitignore-only and cannot affect a build.

Local verification, named

  • Type-substitution probe, recorded below.
  • git status --porcelain clean on the branch at merge time.
  • Tracked paths matching .agents, .claude, or CLAUDE.md on this head: 0,
    identical to main. Enumerated from the head tree via the git trees API and
    from git ls-tree -r origin/main, two independent means, reconciled.

Type-substitution probe

For each ignored agent path, the path was replaced with a different filesystem
type and BOTH assertions were checked:

  • git check-ignore -v still matches
  • git status --porcelain does NOT list it

Run in a throwaway clone at ec950dc, so no working tree was mutated. Types
exercised: directory, regular file, and symlink pointing outside any repository.

.agents  as dir      PASS  check-ignore=match  status=absent
.agents  as file     PASS  check-ignore=match  status=absent
.agents  as symlink  PASS  check-ignore=match  status=absent
.claude  as dir      PASS  check-ignore=match  status=absent
.claude  as file     PASS  check-ignore=match  status=absent
.claude  as symlink  PASS  check-ignore=match  status=absent

Self-verifying input, exercised in the same run. The trailing-slash form of
the same rule (.agents/, .claude/) against the symlink case, which must
fail:

.agents/  as symlink  FAIL  check-ignore=NO-MATCH  status=LISTED
.claude/  as symlink  FAIL  check-ignore=NO-MATCH  status=LISTED

The control failed as required. That is what makes the pass above meaningful: it
distinguishes a correct rule from a check that did not run.

check-ignore alone is insufficient, and that is the specific defect F-022
records. Both assertions are required, which is why both are asserted here.


CORRECTION, added after merge

The CI status section above is wrong, and this correction supersedes it.

That section asserted the outage was ruled out, on the reasoning that Actions had
recovered and that a re-fired event producing nothing could not be explained by a
recovered outage. Both the premise and the conclusion were wrong.

What actually happened. The re-fired pull_request event did produce a run.
It arrived at 22:25:05Z, roughly 56 minutes after the close and reopen, and well
after this PR had already been merged. Run 31128983196, event pull_request,
head ec950dcc, conclusion success.

CI IS MEASURED, AND IT IS GREEN on the exact head that was merged.

Why the earlier reasoning failed. Actions was not recovered. It was still in a
major outage at 22:18Z, with webhook triggers deliberately throttled and, in
GitHub's own words, many push and pull request events not yet triggering new
workflow runs. The 20:47Z job in a sibling repository that was read as evidence of
recovery was a queued event draining, not a healthy pipeline. A single successful
job is not evidence that event delivery is healthy, and it was treated as such.

Withdrawn: the organization-level Actions policy hypothesis. It was recorded as
UNKNOWN rather than asserted, which was right, but it is no longer needed. Delivery
latency under the outage explains the observation. The 403 on reading the org policy
remains true and remains unread; it is simply not evidence of anything here.

Retained and still correct: the trigger analysis (base=main, workflow present
and identical on the head ref, no paths filter, no job guard), the baseline note
that this is the first PR ever opened here so prior pull_request absence carries
no information, and the entire type-substitution probe including its control.

The lesson, stated so it survives this PR. The failure was declaring a negative
from an interval of silence. Twenty nine minutes of no run was treated as a result
when it was only an unfinished wait, and a sibling repository's single green job was
promoted into a systemwide recovery claim it could not support. Absence within an
observation window is UNKNOWN, not a finding, and the authoritative status source
should have been read before any cause was written down.

…s symlink-safe

This repository is public. The agent workspace and local agent config were
present in the working tree, excluded only by ignore rules. Their contents are
now stored outside any repository and reached through symlinks, so the bytes
are not in a public tree at all and a forced add would commit a path rather
than the content.

Drops the trailing slashes at the same time. A trailing-slash rule matches a
directory only, so `.agents/` stopped matching the moment the directory became
a symlink, and the path fell out of the ignore set silently. Slashless entries
match a directory, a file, or a symlink.

No change to tracked content: zero paths under these entries before or after,
and no commit in this repository has ever touched them.
@andrewschristison
andrewschristison merged commit e722c34 into main Aug 6, 2026
@andrewschristison
andrewschristison deleted the gitignore-symlink-safe branch August 6, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant