Skip to content

hygiene: gitignore .claude/worktrees/ — parallel-research scratch#40

Merged
AceHack merged 1 commit intomainfrom
hygiene/gitignore-claude-worktrees
Apr 21, 2026
Merged

hygiene: gitignore .claude/worktrees/ — parallel-research scratch#40
AceHack merged 1 commit intomainfrom
hygiene/gitignore-claude-worktrees

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented Apr 21, 2026

Summary

Adds .claude/worktrees/ to .gitignore so the no-empty-dirs CI gate stops flagging the parent dir left behind after a Claude Code parallel-worktree research session.

Found by

The Round-36-committed-P0 "Empty-folder allowlist review" — one of two P0s queued in docs/CURRENT-ROUND.md §Round-36 committed P0. Ran bash tools/lint/no-empty-dirs.sh --list locally; .claude/worktrees appeared as an unexpected flagged entry.

Why gitignore and not allowlist

The allowlist (tools/lint/no-empty-dirs.allowlist) is for load-bearing empty dirs — paths that install.sh creates and that tools later populate (tools/alloy/classes, tools/tla/specs/states). .claude/worktrees/ is harness-private runtime scratch; it shouldn't be a repo concern at all. .gitignore is the right layer per the lint script's "Excluded from scan: any path matched by .gitignore" rule.

Cross-reference

Origin context: docs/research/parallel-worktree-safety-2026-04-22.md §9 — the incident log for the parallel-worktree-safety research pass that produced this leftover state.

Side-finding (not in scope for this PR)

While running the lint locally on macOS bash 3.2.57, the script crashes with FILTERED[@]: unbound variable at line 111 when the FILTERED array is empty (happens after removing .claude/worktrees/ and when the allowlist targets don't exist yet). CI (Ubuntu bash 5.x) doesn't trigger this — empty-array deref under set -u is bash-3.2-specific. Belongs to FACTORY-HYGIENE #48 (cross-platform parity hygiene); noting here for findability but leaving for a separate fix.

Test plan

  • git check-ignore .claude/worktrees returns path on this branch
  • .claude/worktrees/ absent from git ls-files
  • CI no-empty-dirs gate green

🤖 Generated with Claude Code

…atch

Found during the Round-36-committed-P0 periodic allowlist audit for
tools/lint/no-empty-dirs.sh: the lint flagged .claude/worktrees/
(new empty dir, not in allowlist, not in .gitignore). Origin is the
parallel-worktree-safety research session — the harness creates the
parent dir when it takes out a git worktree and can leave it as an
empty dir after the worktree is torn down.

Right fix is .gitignore rather than allowlist: the dir is harness-
private runtime scratch, not a load-bearing empty dir (which is the
allowlist's job — Alloy / TLC output paths that install.sh creates
and tools fill). The no-empty-dirs script's "Excluded from scan:
any path matched by .gitignore" rule keeps the CI gate quiet.

Cross-ref: docs/research/parallel-worktree-safety-2026-04-22.md
§9 incident log is the full context for why .claude/worktrees/
appears in the first place.
Copilot AI review requested due to automatic review settings April 21, 2026 12:21
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review any files in this pull request.

@AceHack AceHack merged commit 307fe67 into main Apr 21, 2026
10 of 11 checks passed
@AceHack AceHack deleted the hygiene/gitignore-claude-worktrees branch April 21, 2026 12:25
AceHack added a commit that referenced this pull request Apr 21, 2026
…ngs-as-code + drift detector

Executes the org migration (AceHack/Zeta -> Lucent-Financial-Group/Zeta)
that was filed as HB-001, then lands the hygiene pattern the migration
taught us: GitHub has click-ops surfaces with no native declarative
config (rulesets, security-and-analysis, Actions variables, Pages,
CodeQL setup), and the transfer code path silently flipped
secret_scanning + secret_scanning_push_protection from enabled to
disabled. Re-enabled same session; built detector so the next silent
drift is not silent.

Shape of what landed:

- docs/GITHUB-SETTINGS.md: human-readable narrative declaration of
  every click-ops setting, pointing at the machine-readable JSON.
- tools/hygiene/github-settings.expected.json: canonical expected
  state, normalized output of the snapshot script.
- tools/hygiene/snapshot-github-settings.sh: produces the normalized
  JSON from live gh api for any repo (--repo flag).
- tools/hygiene/check-github-settings-drift.sh: diffs live vs
  expected; exit 1 on drift with resolution instructions.
- .github/workflows/github-settings-drift.yml: weekly cron
  (Mondays 14:17 UTC) + workflow_dispatch + PR-triggered when the
  expected snapshot or tooling changes. Uses only trusted
  first-party context (secrets.GITHUB_TOKEN, github.repository)
  passed via env vars into quoted run-block invocation.
- docs/FACTORY-HYGIENE.md row #40 with full cadence / owner /
  checks / output / source-of-truth specification.
- docs/HUMAN-BACKLOG.md: HB-001 State=Open -> Resolved with the
  two-line drift summary baked into the Resolution cell.
- docs/ROUND-HISTORY.md: late-Round-44 arc paragraph documenting
  the migration, the drift, and the new hygiene class.

Pattern generalizes: any click-ops platform (AWS / GCP / Slack /
org-level settings) gets the same markdown-declaration +
cadenced-diff treatment when adopted. Settings-as-code-by-
convention beats Terraform / Probot / Pulumi for small repos until
friction demands the heavier tooling.

Companion memories: feedback_github_settings_as_code_declarative_
checked_in_file.md (the pattern) + feedback_blast_radius_pricing_
standing_rule_alignment_signal.md (Aaron's praise of the
confirm-before-hard-to-reverse discipline, reframing it as a Zeta
product-feature signal per the retractable-contract ledger).
AceHack added a commit that referenced this pull request Apr 21, 2026
…ngs-as-code + drift detector (#45)

* Resolve HB-001: transfer to Lucent-Financial-Group; land GitHub-settings-as-code + drift detector

Executes the org migration (AceHack/Zeta -> Lucent-Financial-Group/Zeta)
that was filed as HB-001, then lands the hygiene pattern the migration
taught us: GitHub has click-ops surfaces with no native declarative
config (rulesets, security-and-analysis, Actions variables, Pages,
CodeQL setup), and the transfer code path silently flipped
secret_scanning + secret_scanning_push_protection from enabled to
disabled. Re-enabled same session; built detector so the next silent
drift is not silent.

Shape of what landed:

- docs/GITHUB-SETTINGS.md: human-readable narrative declaration of
  every click-ops setting, pointing at the machine-readable JSON.
- tools/hygiene/github-settings.expected.json: canonical expected
  state, normalized output of the snapshot script.
- tools/hygiene/snapshot-github-settings.sh: produces the normalized
  JSON from live gh api for any repo (--repo flag).
- tools/hygiene/check-github-settings-drift.sh: diffs live vs
  expected; exit 1 on drift with resolution instructions.
- .github/workflows/github-settings-drift.yml: weekly cron
  (Mondays 14:17 UTC) + workflow_dispatch + PR-triggered when the
  expected snapshot or tooling changes. Uses only trusted
  first-party context (secrets.GITHUB_TOKEN, github.repository)
  passed via env vars into quoted run-block invocation.
- docs/FACTORY-HYGIENE.md row #40 with full cadence / owner /
  checks / output / source-of-truth specification.
- docs/HUMAN-BACKLOG.md: HB-001 State=Open -> Resolved with the
  two-line drift summary baked into the Resolution cell.
- docs/ROUND-HISTORY.md: late-Round-44 arc paragraph documenting
  the migration, the drift, and the new hygiene class.

Pattern generalizes: any click-ops platform (AWS / GCP / Slack /
org-level settings) gets the same markdown-declaration +
cadenced-diff treatment when adopted. Settings-as-code-by-
convention beats Terraform / Probot / Pulumi for small repos until
friction demands the heavier tooling.

Companion memories: feedback_github_settings_as_code_declarative_
checked_in_file.md (the pattern) + feedback_blast_radius_pricing_
standing_rule_alignment_signal.md (Aaron's praise of the
confirm-before-hard-to-reverse discipline, reframing it as a Zeta
product-feature signal per the retractable-contract ledger).

* fix(lints): unblock PR #45 — markdownlint, actionlint, shellcheck

- docs/ROUND-HISTORY.md: `+` at line-start parsed as bullet by
  markdownlint (MD032); replace with "and" in the two affected
  wrap points.
- .github/workflows/github-settings-drift.yml: drop
  `administration: read` — actionlint v1.7.x does not list it
  as a recognised scope; the default GITHUB_TOKEN on public
  repos covers the endpoints we read. Comment documents the
  trade + the fact that the PR-triggered drift check would
  surface a missing scope immediately.
- tools/hygiene/snapshot-github-settings.sh: drop dead
  `rulesets_json` assignment (SC2034) — the `ruleset_details`
  loop below returns the full per-ruleset data used in the
  final jq output.

Aaron's follow-up "you also didn't get half the settings you
miss most of them lol it's okay" is acknowledged — a coverage
expansion PR will follow this one. The current snapshot is a
first pass; PR #45 lands the *pattern* (decl + drift detector
+ hygiene row) and we iterate on coverage.

* fix: restore administration:read + actionlint -ignore workaround

The first PR #45 lint fix dropped `administration: read` hoping
the default GITHUB_TOKEN would cover the rulesets / branch
protection / security_and_analysis endpoints — wrong. The CI run
of the drift detector itself returned six HTTP 403s on those
endpoints, then `jq --argjson` on the empty results produced
`invalid JSON text` and the detector failed with exit 2 (tooling
error, not drift).

Fix:

- Restore `administration: read` to
  .github/workflows/github-settings-drift.yml — it's a valid
  GitHub Actions permission scope that actionlint v1.7.x simply
  hasn't added to its allow-list yet (confirmed against v1.7.12
  local and v1.7.7 CI: both reject it).
- Teach the gate lint-workflows job to pass
  `-ignore 'unknown permission scope "administration"'` to
  actionlint. Surgical: targets exactly the known false-positive
  string; removable when actionlint catches up. Comment points at
  the upstream tracker.

The meta-hygiene here is intact: my own drift detector caught my
own wrong hypothesis on the very next PR push. Detector-outlives-
fix rule confirms — the class of "my workflow asked for too few
permissions" gets an automated regression signal now.

* snapshot: expand coverage — repo-level keys + 5 new endpoints

Aaron 2026-04-21: "you also didn't get half the setting you
miss most of them lol it's okay" — accurate critique of PR #45
first-pass coverage. The declarative-settings-as-code pattern
only pays off if the settings are actually in the declaration.

Additions to `tools/hygiene/snapshot-github-settings.sh`:

Repo-level keys previously uncaptured:
- `allow_forking` (CRITICAL — gates the fork-based PR
  workflow that Aaron proposed the same round)
- `description`, `homepage`, `topics` (discovery metadata)
- `has_downloads`, `has_pages`, `has_pull_requests` (feature
  toggles)
- `merge_commit_message`, `merge_commit_title` (even with
  merge-commit off, the format defaults are a policy value)
- `use_squash_pr_title_as_default`
- `is_template`, `archived`, `disabled` (lifecycle state)
- `pull_request_creation_policy`
- `custom_properties` (org-level custom repo properties)

Security-surface endpoints previously unqueried:
- `/topics` (captured as `.topics` at top level)
- `/automated-security-fixes` (Dependabot auto-PR state)
- `/private-vulnerability-reporting` (security-tab reports
  enable state)
- `/interaction-limits` (rate-limit / cooldown state; null
  when inactive — note the `gh api --jq` empty-output quirk
  on object-length-0, handled via raw-fetch-then-jq two-step)
- `/autolinks` (external issue-tracker linking)
- `/vulnerability-alerts` (Dependabot alerts; endpoint returns
  204/404 not JSON — handled via status-code probe)

All captured under a new top-level `security:` subsection
(vulnerability_alerts_enabled + automated_security_fixes +
private_vulnerability_reporting) plus top-level `topics`,
`interaction_limits`, `autolinks` for orthogonal surfaces.

`tools/hygiene/github-settings.expected.json` re-snapshotted
against live state (now 237 lines, up from 196). Verified
`check-github-settings-drift.sh` returns "no drift" against
the live repo.

`docs/GITHUB-SETTINGS.md` §"Repo-level toggles" expanded to
cover the new fields + new §"Repo security extras"
subsection. Cross-references the fork-based-workflow memory
because `allow_forking` is the gating setting.

* fix(pr#45): address Copilot actionable findings at source

Script robustness:
- snapshot + check scripts: guard `--repo` / `--expected` arg parsing
  against missing value (previously `$2` unbound under `set -u`)
- snapshot: sort rulesets by id before emission (stabilizes output
  across GitHub-side listing-order churn)
- snapshot: clarify exit-code docstring (null-tolerant optional
  endpoints vs. loud-fail required endpoints)

Workflow alignment:
- github-settings-drift.yml: pin actions/checkout to v6.0.2 SHA
  matching gate.yml + codeql.yml

Doc polish:
- FACTORY-HYGIENE.md: reorder row 39 before row 40
- GITHUB-SETTINGS.md: drop broken external-memory cross-references;
  fold diagnostic command for code_scanning rule inline; shift
  "Motivation (<name>, date)" → "Motivation (human maintainer, date)"
  per name-attribution BP
- HUMAN-BACKLOG.md: drop external-memory cross-reference from HB-001
- ROUND-HISTORY.md: inline the "blast-radius praise" content instead
  of cross-referencing by path; external auto-memory paths don't
  resolve for non-Claude readers

The memory/*.md cross-references were relics of when committed docs
pointed at Claude's per-project auto-memory (`~/.claude/projects/
<slug>/memory/`) as if it were an in-repo directory. For humans and
Copilot those paths are broken links; for Claude the information is
reachable via the memory tool without a doc cross-reference. Strip
them where they appear inline in prose; the established table
convention in FACTORY-HYGIENE/ROUND-HISTORY (Origin column) keeps
its references for pattern-consistency with the 39+ pre-existing
rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

2 participants