@@ -14,6 +14,8 @@ import (
1414 "time"
1515
1616 "github.com/entireio/cli/cmd/entire/cli/agent"
17+ _ "github.com/entireio/cli/cmd/entire/cli/agent/claudecode" // register claude-code so its .claude protected dir is discoverable
18+ "github.com/entireio/cli/cmd/entire/cli/agent/types"
1719 "github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
1820 "github.com/entireio/cli/cmd/entire/cli/paths"
1921 "github.com/entireio/cli/cmd/entire/cli/testutil"
@@ -103,6 +105,99 @@ func TestCopyMetadataDir_SkipsSymlinks(t *testing.T) {
103105 }
104106}
105107
108+ // fakePluginAgent is a minimal agent stub used to prove that protected dirs
109+ // and files reported by an external-plugin-style agent (via the AllProtectedDirs
110+ // / AllProtectedFiles union) are honored by the first-checkpoint path, not just
111+ // the built-in claude-code .claude dir.
112+ type fakePluginAgent struct {}
113+
114+ var (
115+ _ agent.Agent = (* fakePluginAgent )(nil )
116+ _ agent.ProtectedFilesProvider = (* fakePluginAgent )(nil )
117+ )
118+
119+ func (fakePluginAgent ) Name () types.AgentName { return "terminalhire-plugin" }
120+ func (fakePluginAgent ) Type () types.AgentType { return "TerminalHire" }
121+ func (fakePluginAgent ) Description () string { return "fake external plugin for tests" }
122+ func (fakePluginAgent ) IsPreview () bool { return true }
123+ func (fakePluginAgent ) ProtectedDirs () []string { return []string {".terminalhire" } }
124+ func (fakePluginAgent ) ProtectedFiles () []string { return []string {".terminalhirerc" } }
125+ func (fakePluginAgent ) GetSessionID (* agent.HookInput ) string { return "" }
126+
127+ func (fakePluginAgent ) DetectPresence (context.Context ) (bool , error ) { return false , nil }
128+ func (fakePluginAgent ) ReadTranscript (string ) ([]byte , error ) { return nil , nil }
129+ func (fakePluginAgent ) ChunkTranscript (_ context.Context , c []byte , _ int ) ([][]byte , error ) {
130+ return [][]byte {c }, nil
131+ }
132+ func (fakePluginAgent ) ReassembleTranscript (chunks [][]byte ) ([]byte , error ) {
133+ var out []byte
134+ for _ , c := range chunks {
135+ out = append (out , c ... )
136+ }
137+ return out , nil
138+ }
139+ func (fakePluginAgent ) GetSessionDir (string ) (string , error ) { return "" , nil }
140+ func (fakePluginAgent ) ResolveSessionFile (dir , sid string ) string { return dir + "/" + sid }
141+ func (fakePluginAgent ) ReadSession (* agent.HookInput ) (* agent.AgentSession , error ) { return nil , nil } //nolint:nilnil // test stub
142+ func (fakePluginAgent ) WriteSession (context.Context , * agent.AgentSession ) error { return nil }
143+ func (fakePluginAgent ) FormatResumeCommand (string ) string { return "" }
144+
145+ // TestCollectChangedFiles_ExcludesProtectedDirs verifies that the
146+ // first-checkpoint path keeps agent-protected dirs (e.g. .claude) and the
147+ // .entire infrastructure dir out of the checkpoint snapshot, while ordinary
148+ // untracked files are still captured. Regression for protected-dir content
149+ // leaking into the shadow tree on session start.
150+ func TestCollectChangedFiles_ExcludesProtectedDirs (t * testing.T ) {
151+ t .Parallel ()
152+
153+ // Register an external-plugin-style agent so its protected dir/file join the
154+ // AllProtectedDirs/AllProtectedFiles union alongside the built-in .claude.
155+ // Registration is additive and concurrency-safe; no test asserts the exact set.
156+ agent .Register ("terminalhire-plugin" , func () agent.Agent { return fakePluginAgent {} })
157+
158+ tempDir := t .TempDir ()
159+ // Resolve symlinks so the repo root matches git's resolved path.
160+ // On macOS, t.TempDir() returns /var/... but git resolves to /private/var/...
161+ tempDir , err := filepath .EvalSymlinks (tempDir )
162+ require .NoError (t , err )
163+
164+ testutil .InitRepo (t , tempDir )
165+ testutil .WriteFile (t , tempDir , "base.txt" , "base" )
166+ testutil .GitAdd (t , tempDir , "base.txt" )
167+ testutil .GitCommit (t , tempDir , "init" )
168+
169+ // Disable any global core.excludesFile so a developer/CI-runner gitignore
170+ // convention (e.g. one that ignores .claude) can't mask the leak. The fix
171+ // must exclude protected dirs on its own, independent of gitignore state.
172+ cfgCmd := exec .CommandContext (context .Background (), "git" , "config" , "core.excludesFile" , os .DevNull )
173+ cfgCmd .Dir = tempDir
174+ require .NoError (t , cfgCmd .Run ())
175+
176+ // Planted untracked, non-gitignored files.
177+ testutil .WriteFile (t , tempDir , ".claude/marker.txt" , "MARKER-secret" ) // built-in agent-protected dir
178+ testutil .WriteFile (t , tempDir , ".terminalhire/profile.json" , "MARKER-plugin" ) // plugin-protected dir
179+ testutil .WriteFile (t , tempDir , ".terminalhirerc" , "MARKER-plugin-file" ) // plugin-protected file
180+ testutil .WriteFile (t , tempDir , ".entire/state.json" , "{}" ) // infrastructure
181+ testutil .WriteFile (t , tempDir , "src/keep.txt" , "user work" ) // ordinary
182+
183+ repo , err := git .PlainOpen (tempDir )
184+ require .NoError (t , err )
185+
186+ result , err := collectChangedFiles (context .Background (), repo )
187+ require .NoError (t , err )
188+
189+ require .NotContains (t , result .Changed , ".claude/marker.txt" ,
190+ "built-in agent protected dir content must not be captured into the checkpoint" )
191+ require .NotContains (t , result .Changed , ".terminalhire/profile.json" ,
192+ "external-plugin protected dir content must not be captured into the checkpoint" )
193+ require .NotContains (t , result .Changed , ".terminalhirerc" ,
194+ "external-plugin protected file must not be captured into the checkpoint" )
195+ require .NotContains (t , result .Changed , ".entire/state.json" ,
196+ "infrastructure dir must not be captured into the checkpoint" )
197+ require .Contains (t , result .Changed , "src/keep.txt" ,
198+ "ordinary untracked files must still be captured" )
199+ }
200+
106201// TestWriteCommitted_AgentField verifies that the Agent field is written
107202// to both metadata.json and the commit message trailer.
108203func TestWriteCommitted_AgentField (t * testing.T ) {
0 commit comments