fix(checkpoint): exclude protected dirs from first-checkpoint snapshot · Entire
fix(checkpoint): exclude protected dirs from first-checkpoint snapshot
262fd3f·
pjbgf·2d ago·5 files·+209 added/-7 removed
The first checkpoint of a session collected changed files via a raw
git status parse in collectChangedFiles that only skipped .entire/
(IsInfrastructurePath). Agent-declared protected dirs/files — built-in
(.claude) and external protocol-v1 plugins (protected_dirs) — were
captured into the shadow-branch tree on session start, unlike the
session-tracking and rewind paths which already honor them.
Add isProtectedCheckpointPath, mirroring shouldIgnoreSessionTrackingPath the two can't share code: cli imports checkpoint, and apply it at the three filter sites in collectChangedFiles.
Make path matching OS-based so case-insensitive filesystems exclude correctly: add paths.CaseInsensitiveFS (windows/darwin), paths.Equal, and fold case in IsSubpath on those platforms. On case-sensitive Linux the behavior is unchanged. Route protected-file equality in state.go and the new checkpoint helper through paths.Equal.
Regression tests: checkpoint first-checkpoint path (built-in + external plugin protected dir/file) and OS-aware paths unit tests.
Assisted-by: Claude Opus 4.8 noreply@anthropic.com Signed-off-by: Paulo Gomes paulo@entire.io
Sessions
01KXK2W6DRHMN0WSE7M51XZVMEView transcript
Changes
5
cmd/entire/cli
checkpoint
Mcheckpoint_test.go+95
Mephemeral.go+34/-5
paths
Mpaths.go+35
Mpaths_test.go+44
Mstate.go+1/-2
13 unmodified lines
// fakePluginAgent is a minimal agent stub used to prove that protected dirs
// and files reported by an external-plugin-style agent (via the AllProtectedDirs
// / AllProtectedFiles union) are honored by the first-checkpoint path, not just
// the built-in claude-code .claude dir.
type fakePluginAgent struct{}
var (
_ agent.Agent = (*fakePluginAgent)(nil)
_ agent.ProtectedFilesProvider = (*fakePluginAgent)(nil)
)
func (fakePluginAgent) Name() types.AgentName { return "terminalhire-plugin" }
func (fakePluginAgent) Type() types.AgentType { return "TerminalHire" }
func (fakePluginAgent) Description() string { return "fake external plugin for tests" }
func (fakePluginAgent) IsPreview() bool { return true }
func (fakePluginAgent) ProtectedDirs() []string { return []string{ ".terminalhire" } }
func (fakePluginAgent) ProtectedFiles() []string { return []string{ ".terminalhirerc" } }
func (fakePluginAgent) GetSessionID(*agent.HookInput) string { return "" }
// TestCollectChangedFiles_ExcludesProtectedDirs verifies that the
// first-checkpoint path keeps agent-protected dirs (e.g. .claude) and the
// .entire infrastructure dir out of the checkpoint snapshot, while ordinary
// untracked files are still captured. Regression for protected-dir content
// leaking into the shadow tree on session start.
func TestCollectChangedFiles_ExcludesProtectedDirs(t *testing.T) {
t.Parallel()
// Register an external-plugin-style agent so its protected dir/file join the
// AllProtectedDirs/AllProtectedFiles union alongside the built-in .claude.
// Registration is additive and concurrency-safe; no test asserts the exact set.
agent.Register("terminalhire-plugin", func() agent.Agent { return fakePluginAgent{} })
tempDir := t.TempDir()
// Resolve symlinks so the repo root matches git's resolved path.
// On macOS, t.TempDir() returns /var/... but git resolves to /private/var/...
tempDir, err := filepath.EvalSymlinks(tempDir)
require.NoError(t, err)
testutil.InitRepo(t, tempDir)
testutil.WriteFile(t, tempDir, "base.txt", "base")
testutil.GitAdd(t, tempDir, "base.txt")
testutil.GitCommit(t, tempDir, "init")
// Disable any global core.excludesFile so a developer/CI-runner gitignore
// convention (e.g. one that ignores .claude) can't mask the leak. The fix
// must exclude protected dirs on its own, independent of gitignore state.
cfgCmd := exec.CommandContext(context.Background(), "git", "config", "core.excludesFile", os.DevNull)
cfgCmd.Dir = tempDir
require.NoError(t, cfgCmd.Run())
// Planted untracked, non-gitignored files.
testutil.WriteFile(t, tempDir, ".claude/marker.txt", "MARKER-secret") // built-in agent-protected dir
testutil.WriteFile(t, tempDir, ".terminalhire/profile.json", "MARKER-plugin") // plugin-protected dir
testutil.WriteFile(t, tempDir, ".terminalhirerc", "MARKER-plugin-file") // plugin-protected file
testutil.WriteFile(t, tempDir, ".entire/state.json", "{}"); // infrastructure
testutil.WriteFile(t, tempDir, "src/keep.txt", "user work") // ordinary
repo, err := git.PlainOpen(tempDir)
require.NoError(t, err)
result, err := collectChangedFiles(context.Background(), repo)
require.NoError(t, err)
require.NotContains(t, result.Changed, ".claude/marker.txt",
"built-in agent protected dir content must not be captured into the checkpoint")
require.NotContains(t, result.Changed, ".terminalhire/profile.json",
"external-plugin protected dir content must not be captured into the checkpoint")
require.NotContains(t, result.Changed, ".terminalhirerc",
"external-plugin protected file must not be captured into the checkpoint")
require.NotContains(t, result.Changed, ".entire/state.json",
"infrastructure dir must not be captured into the checkpoint")
require.Contains(t, result.Changed, "src/keep.txt",
"ordinary untracked files must still be captured")
}
// TestWriteCommitted_AgentField verifies that the Agent field is written
// to both metadata.json and the commit message trailer.
func TestWriteCommitted_AgentField(t *testing.T) {
"""
### Implementation of additional functions omitted for brevity