Retire the .entire/metadata deny rule and release staged transcripts - #2258
Conversation
.entire/metadata/<session>/ is a staging buffer for the checkpoint writer, not a store. clearFilesystemPrompt removed prompt.txt once a session's work was condensed, but nothing ever removed full.jsonl, so every session left its full transcript in the worktree permanently after the content was already committed and pushed. Measured 460 MB across 392 session directories in one checkout, largest single transcript 10.2 MB, with sessions going back nine months. Releasing it is safe: the file is rewritten wholesale from the agent's own transcript on every Stop, and the only readers are the two tree-building walks that run before this point. Every other reader of a session's transcript goes through a git tree. sessionHasNewContent compares against the shadow-tree blob, and the incremental redaction cache is keyed by prefix content hash in the git common dir, so neither depends on this path existing. Prompt and transcript are now released together by clearFilesystemStagedFiles, because they are one operation under one condition rather than two: the call site's guard is shared for a real reason, since carry-forward files mean both must survive for the next condensation. One directory walk covers all three names instead of one walk per file on the PostCommit hook path. This only prevents future growth. Reclaiming what has already accumulated needs a separate sweep over fully-condensed sessions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M1KMXAH0D66WP01ZWMSXRADN
The Stop-hook section claimed Entire writes summary.txt ("the last
assistant message, used as checkpoint summary"). Nothing has written it
for some time -- the name has zero references in the Go source -- yet 232
of them sit on disk in one checkout, alongside 168 context.md files that
PR #572 removed.
Replace the stale entry with what the hook actually stages, and say that
both files are a buffer whose durable copy lives in the checkpoint tree.
scripts/migrate-sessions.sh keeps its context.md reference: it migrates
the old format that legitimately has one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M1KMXNW2R2VKE2WBH2HN1XMJ
Entire installed Read(./.entire/metadata/**) into Claude Code and Factory Droid permission configs. A deny rule is a hard block, not a hint: rules resolve deny then ask then allow, first match wins, and a deny rule cannot carry allowlist exceptions. Claude Code applies Read deny rules to file-reading Bash commands too, so a recursive grep from the repo root -- or a command that merely names the path -- is refused and has to be approved by hand, which defeats unattended permission modes for a whole class of ordinary commands. `git check-ignore -v .entire/metadata` is refused, and it reads nothing. It also protected less than it appeared to. It guarded a staging buffer whose durable copy lives in the checkpoint tree, which no permission rule covers -- the same transcripts are readable with `git show entire/checkpoints/v1:...`. And .entire/.gitignore already lists metadata/, which Claude Code's Grep honors, so the accidental-bulk-read case was already covered. Glob does not honor gitignore, but Glob returns names, not content. Install now removes the rule instead of adding it, unconditionally on a plain `entire enable` like the existing stale-hook migration. `entire doctor` detects and repairs it. Removal matches only the exact string Entire wrote, so a user's own deny rules are untouched, and an emptied deny array is dropped rather than left behind. The SessionStart hook only reports it and points at doctor. It deliberately does not write: the agent's config is normally tracked in git, so a hook repairing it would dirty the worktree unprompted and could land the edit in the user's next checkpoint commit, and it would ping-pong across a team on mixed CLI versions. Not replaced with a PreToolUse hook on Read -- that fires a subprocess for every file the agent reads, on a hook path that already dominates turn cost. Also collapses four copies of the same deny-rule filter (install and uninstall, in both agents) into one shared helper, and updates this repo's own settings.json, which the dogfood test pins. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M1KMY4XMYH6AZ7CZXHSAMH90
There was a problem hiding this comment.
🟡 Changes recommended
A few tracked/documented references and in-code comments are out of sync with the renamed staged-file cleanup behavior, and the repo’s tracked Claude settings can be cleaned up to avoid an empty permissions block.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adjusts how Entire handles .entire/metadata by (1) treating it explicitly as a staging buffer whose transient files are cleaned up after successful condensation, and (2) retiring the agent permission deny rule that blocked reads of .entire/metadata/**, plus adding “healing” paths via enable, doctor, and a SessionStart notice.
Changes:
- Remove the retired
Read(./.entire/metadata/**)deny rule from Claude Code and Factory AI Droid configs, and add detection/repair utilities plus doctor integration. - Release staged session files (
full.jsonl,prompt.txt, legacyfull.log) after condensation when no carry-forward files remain, with tests covering the new behavior. - Update integration tests/docs to reflect the new permission and staging-file behavior.
File summaries
| File | Description |
|---|---|
| docs/architecture/claude-hooks-integration.md | Updates Claude hook documentation to describe staged transcript/prompt behavior. |
| cmd/entire/cli/strategy/phase_postcommit_test.go | Adjusts PostCommit tests to assert staged files are released after condensation. |
| cmd/entire/cli/strategy/manual_commit_hooks.go | Switches PostCommit cleanup call to clear both prompt and transcript staged files. |
| cmd/entire/cli/strategy/manual_commit_condensation.go | Adds unified staged-file cleanup and shared session metadata filename helper. |
| cmd/entire/cli/strategy/manual_commit_condensation_test.go | Adds unit tests pinning staged-file cleanup behavior and idempotency. |
| cmd/entire/cli/lifecycle.go | Adds SessionStart warning when a repo still has the retired deny rule. |
| cmd/entire/cli/integration_test/setup_factoryai_hooks_test.go | Updates Factory AI Droid setup test to assert deny rule is not installed. |
| cmd/entire/cli/integration_test/agent_test.go | Updates agent installation tests to assert deny rule is not installed. |
| cmd/entire/cli/doctor.go | Adds doctor check to remove the retired deny rule and report the fix. |
| cmd/entire/cli/agent/permissions.go | Introduces shared permission-rule detection/removal + config-owner interface. |
| cmd/entire/cli/agent/permissions_test.go | Adds tests for deny-rule detection/removal logic and invariants. |
| cmd/entire/cli/agent/factoryaidroid/hooks.go | Removes deny-rule installation; adds migration removal and PermissionConfigOwner plumbing. |
| cmd/entire/cli/agent/factoryaidroid/hooks_test.go | Updates Factory AI Droid tests to validate deny rule is removed/preserved appropriately. |
| cmd/entire/cli/agent/claudecode/hooks.go | Removes deny-rule installation; adds migration removal and PermissionConfigOwner plumbing. |
| cmd/entire/cli/agent/claudecode/hooks_test.go | Updates Claude Code tests to validate deny rule is removed/preserved appropriately. |
| .claude/settings.json | Removes the deny rule from the repo’s tracked Claude settings. |
Review details
- Files reviewed: 16/16 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review found that RemoveMetadataDenyRule clears the `deny` key but cannot
delete the `permissions` key that contains it -- it is handed only the
inner object. UninstallHooks and RepairRetiredMetadataDenyRule both
delete an emptied block; the two InstallHooks write paths did not, so a
plain install that stripped the last deny rule left `"permissions": {}`
behind. This PR's own .claude/settings.json diff was the evidence.
Both write paths now delete the key when the block is empty, and the doc
comment on RemoveMetadataDenyRule no longer claims a cleanup that belongs
to its callers.
Also corrects two references left stale by the earlier merge of
clearFilesystemPrompt and clearFilesystemTranscript: a comment naming
RemoveNoSymlinks, which that code no longer calls, and the hook docs
still naming clearFilesystemPrompt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M1KNXE2RZ1XFKT6D9KJKXFHX
https://entire.io/gh/entireio/cli/trails/1230
Three related changes to
.entire/metadata: it turns out to be a staging buffer that nothing ever emptied, and the permission rule guarding it was breaking unattended permission modes for a whole class of ordinary commands.1. The staged transcript is released after condensation
clearFilesystemPromptremovedprompt.txtonce a session's work was condensed, but nothing ever removedfull.jsonl. Every session left its full transcript in the worktree permanently, after the content was already committed and pushed.Measured in one checkout: 460 MB across 392 session directories, largest single transcript 10.2 MB, with sessions going back nine months. Around 400 of those 1124 files were already dead formats — 168
context.md(removed by #572) and 232summary.txt(no code has written it for some time).Releasing it is safe, and I checked each coupling rather than assuming:
lifecycle.gorewrites the file wholesale from the agent's own transcript on every StopaddDirectoryToChanges(shadow) andcopyEntireMetadataDir(v1), both of which run before this point, plusreview_context.goforprompt.txt. Every other reader goes through a git treesessionHasNewContent?Prompt and transcript are now released together, because they are one operation under one condition rather than two — the call site's guard is shared for a real reason, since carry-forward files mean both must survive for the next condensation. One directory walk covers all three names instead of one walk per file on the PostCommit hook path.
2. The
Read(./.entire/metadata/**)deny rule is retiredA
denyrule is a hard block, not a hint. Per the permissions docs ("Read and Edit" section), rules resolve deny → ask → allow, first match wins, and "a deny rule can't carry allowlist exceptions" — so there is no way to soften it. The same section states Read deny rules apply "to file commands Claude Code recognizes in Bash, such ascat,head,tail, andsed", so a recursive grep from the repo root is refused and has to be approved by hand. Auto mode's classifier only sees actions that would prompt, never a block, so it cannot get past this.The match is broader than "commands that read those files":
git check-ignore -v .entire/metadatais refused, and it reads nothing. Naming the path is enough.It also protected less than it appeared to:
git show entire/checkpoints/v1:.....entire/.gitignorealready listsmetadata/, and the tools reference says "Grep respects.gitignore, so gitignored files are skipped" — so the accidental-bulk-read case was already covered. Glob does not honor gitignore, but Glob returns names, not content.Deliberately not replaced with a
PreToolUsehook onRead: that fires a subprocess for every file the agent reads, on a hook path that already dominates turn cost.How an existing repo heals
There is no per-repo "the CLI was upgraded" trigger —
curl-bash-post-installonly does shell completion and runs outside a repo, andversioncheckonly notifies. So healing needed a deliberate shape:entire doctorentire doctor. Writes nothingentire agent add <agent>InstallHooks, so it also removes itentire enableCaveat on
enable, found while verifying: removal insideInstallHooksis unconditional (no--forceneeded), butenableitself returns early with "Entire is already enabled" before it ever reachesInstallHooks. So theenabletier does not fire for an already-enabled repo — which is exactly the population carrying the stale rule. That is why the SessionStart notice points atdoctorrather than atenable, anddoctoris the tier I verified end to end. Makingenablere-run installers on an already-enabled repo is a broader behavior change I did not want to make here.The hook deliberately does not repair. The agent's config is normally tracked in git (this repo tracks
.claude/settings.json), so a hook editing it would dirty the worktree unprompted, could land the edit in the user's next checkpoint commit, and would ping-pong across a team on mixed CLI versions.Removal matches only the exact string Entire wrote, so a user's own deny rules are untouched, and an emptied
denyarray is dropped rather than left behind as{}.One judgment call to confirm
checkRetiredDenyRuleis the only doctor check that repairs without asking. My reasoning is ownership — it deletes only a byte-identical copy of a string Entire itself wrote — but it does modify a git-tracked file, and that is a product call rather than mine. Moving it behind doctor's--forceis a one-line change if you would rather it asked.Not covered
The 460 MB already on disk is not reclaimed. This only prevents future growth; existing sessions keep their transcripts because nothing revisits an already-condensed session. Reclaiming them wants the same doctor-tier treatment over
FullyCondensedsessions, with its own safety question about which sessions are provably done. Happy to follow up.Separate pre-existing gap, found while verifying:
agentHookState.unchecked(config.go:228) is populated but has no consumer anywhere, so a repo with an unparseable.claude/settings.jsongets "Git hooks: OK" fromentire doctorand no mention of the broken file. Not introduced here, but it is why an error branch I first wrote turned out to be unreachable. Probably worth its own issue.Test plan
mise run fmt && mise run lint→ 0 issues;mise run test:cipasses (unit + integration + Vogon canary)permissions_test.gocovers removal, near-miss rules left alone, duplicates, unparseabledeny, sibling-key preservation, and that the detector does not mutateTestPermissionConfigOwner_*exercises the repair path end to end through the real agent — including idempotency and an absent configenableclears it;doctorfixes it, is idempotent, preservesBash(rm -rf *)andallow, and drops the wholepermissionsblock when the rule was its only entry; SessionStart prints the notice and leaves the file byte-identicalReviewed for reuse, simplification, efficiency, and altitude before opening — that pass merged the two release functions into one single-walk operation, removed a redundant
Exists()stat from the SessionStart path, made doctor read the config once instead of twice, and deleted one unreachable branch.Addressed from review
The trail finding and Copilot both caught the same real bug from different angles:
RemoveMetadataDenyRuleclears thedenykey but cannot delete thepermissionskey containing it, since it is handed only the inner object — and whileUninstallHooksandRepairRetiredMetadataDenyRuleboth delete an emptied block, the twoInstallHookswrite paths did not. A plain install that stripped the last deny rule left"permissions": {}behind, and this PR's own.claude/settings.jsondiff was the evidence. Both write paths now delete the key when the block is empty, verified against a fresh repo (Entire-owned config loses the key entirely; a config with a user'sallow/denyrules keeps them and loses only the retired rule). The doc comment no longer claims a cleanup that belongs to its callers.Also fixed two references left stale by the earlier merge of the two release functions: a comment naming
RemoveNoSymlinks, which that code no longer calls, and the hook docs still namingclearFilesystemPrompt.🤖 Generated with Claude Code