Add sparse checkout to exclude Claude context files from source repos

Excludes all Claude Code context files to prevent source repo instructions
from interfering with Gas Town agent configuration:
- .claude/       : settings, rules, agents, commands
- CLAUDE.md      : primary context file
- CLAUDE.local.md: personal context file
- .mcp.json      : MCP server configuration

Legacy configurations (only excluding .claude/) are detected and upgraded
by gt doctor --fix.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
julianknutsen
2026-01-06 15:05:19 -08:00
parent fc4b9de02c
commit 81a7d04239
3 changed files with 370 additions and 11 deletions

View File

@@ -635,12 +635,19 @@ func ConfigureSparseCheckout(repoPath string) error {
// Write patterns directly to sparse-checkout file
// (git sparse-checkout set --stdin escapes the ! character incorrectly)
// Exclude all Claude Code context files to prevent source repo instructions
// from interfering with Gas Town agent context:
// - .claude/ : settings, rules, agents, commands
// - CLAUDE.md : primary context file
// - CLAUDE.local.md : personal context file
// - .mcp.json : MCP server configuration
infoDir := filepath.Join(gitDir, "info")
if err := os.MkdirAll(infoDir, 0755); err != nil {
return fmt.Errorf("creating info dir: %w", err)
}
sparseFile := filepath.Join(infoDir, "sparse-checkout")
if err := os.WriteFile(sparseFile, []byte("/*\n!.claude/\n"), 0644); err != nil {
sparsePatterns := "/*\n!/.claude/\n!/CLAUDE.md\n!/CLAUDE.local.md\n!/.mcp.json\n"
if err := os.WriteFile(sparseFile, []byte(sparsePatterns), 0644); err != nil {
return fmt.Errorf("writing sparse-checkout: %w", err)
}
@@ -662,10 +669,33 @@ func ConfigureSparseCheckout(repoPath string) error {
return nil
}
// ExcludedContextFiles lists all Claude context files that should be excluded by sparse checkout.
var ExcludedContextFiles = []string{
".claude",
"CLAUDE.md",
"CLAUDE.local.md",
".mcp.json",
}
// CheckExcludedFilesExist checks if any Claude context files still exist in the repo
// after sparse checkout was configured. These files should have been removed by
// git read-tree, but may remain if they were untracked or modified.
// Returns a list of files that still exist and should be manually removed.
func CheckExcludedFilesExist(repoPath string) []string {
var remaining []string
for _, file := range ExcludedContextFiles {
path := filepath.Join(repoPath, file)
if _, err := os.Stat(path); err == nil {
remaining = append(remaining, file)
}
}
return remaining
}
// IsSparseCheckoutConfigured checks if sparse checkout is enabled and configured
// to exclude .claude/ for a given repo/worktree.
// to exclude Claude Code context files for a given repo/worktree.
// Returns true only if both core.sparseCheckout is true AND the sparse-checkout
// file contains the !.claude/ exclusion pattern.
// file contains all required exclusion patterns.
func IsSparseCheckoutConfigured(repoPath string) bool {
// Check if core.sparseCheckout is true
cmd := exec.Command("git", "-C", repoPath, "config", "core.sparseCheckout")
@@ -685,15 +715,27 @@ func IsSparseCheckoutConfigured(repoPath string) bool {
gitDir = filepath.Join(repoPath, gitDir)
}
// Check if sparse-checkout file exists and excludes .claude/
// Check if sparse-checkout file exists and excludes Claude context files
sparseFile := filepath.Join(gitDir, "info", "sparse-checkout")
content, err := os.ReadFile(sparseFile)
if err != nil {
return false
}
// Check for our exclusion pattern
return strings.Contains(string(content), "!.claude/")
// Check for all required exclusion patterns
contentStr := string(content)
requiredPatterns := []string{
"!/.claude/", // or legacy "!.claude/"
"!/CLAUDE.md", // or legacy without leading slash
}
for _, pattern := range requiredPatterns {
// Accept both with and without leading slash for backwards compatibility
legacyPattern := strings.TrimPrefix(pattern, "/")
if !strings.Contains(contentStr, pattern) && !strings.Contains(contentStr, legacyPattern) {
return false
}
}
return true
}
// WorktreeRemove removes a worktree.