Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions internal/agents/claudecode/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,16 @@ func (a *Adapter) Plan(env agents.Env) (*agents.Plan, error) {
if env.InstallGlobalInstructions {
p.Files = append(p.Files, agents.FileAction{Path: userClaudeMdPath(env.Home), Action: agents.ActionWouldMerge, Keys: []string{"gortex-rules-block"}})
}
if env.Home != "" {
configDir := userClaudeConfigDir(env.Home)
if configDir != "" {
for name := range GlobalSkills {
p.Files = append(p.Files, agents.FileAction{Path: filepath.Join(env.Home, ".claude", "skills", name, "SKILL.md"), Action: agents.ActionWouldCreate})
p.Files = append(p.Files, agents.FileAction{Path: filepath.Join(configDir, "skills", name, "SKILL.md"), Action: agents.ActionWouldCreate})
}
for name := range SlashCommands {
p.Files = append(p.Files, agents.FileAction{Path: filepath.Join(env.Home, ".claude", "commands", name), Action: agents.ActionWouldCreate})
p.Files = append(p.Files, agents.FileAction{Path: filepath.Join(configDir, "commands", name), Action: agents.ActionWouldCreate})
}
for name := range SubAgents {
p.Files = append(p.Files, agents.FileAction{Path: filepath.Join(env.Home, ".claude", "agents", name), Action: agents.ActionWouldCreate})
p.Files = append(p.Files, agents.FileAction{Path: filepath.Join(configDir, "agents", name), Action: agents.ActionWouldCreate})
}
}
return p, nil
Expand Down Expand Up @@ -309,7 +310,7 @@ func installPermissions(w io.Writer, settingsPath string, opts agents.ApplyOpts)
// exist (users may have customised their copy).
func installGlobalSkills(w io.Writer, home string, opts agents.ApplyOpts) ([]agents.FileAction, error) {
out := make([]agents.FileAction, 0, len(GlobalSkills))
skillsDir := filepath.Join(home, ".claude", "skills")
skillsDir := filepath.Join(userClaudeConfigDir(home), "skills")
for name, content := range GlobalSkills {
dir := filepath.Join(skillsDir, name)
path := filepath.Join(dir, "SKILL.md")
Expand All @@ -329,7 +330,7 @@ func installGlobalSkills(w io.Writer, home string, opts agents.ApplyOpts) ([]age
// `gortex install`.
func installGlobalSlashCommands(w io.Writer, home string, opts agents.ApplyOpts) ([]agents.FileAction, error) {
out := make([]agents.FileAction, 0, len(SlashCommands))
dir := filepath.Join(home, ".claude", "commands")
dir := filepath.Join(userClaudeConfigDir(home), "commands")
for name, content := range SlashCommands {
path := filepath.Join(dir, name)
action, err := agents.WriteIfNotExists(w, path, content, opts)
Expand All @@ -347,7 +348,7 @@ func installGlobalSlashCommands(w io.Writer, home string, opts agents.ApplyOpts)
// re-installs. Mirrors installGlobalSkills / installGlobalSlashCommands.
func installGlobalSubAgents(w io.Writer, home string, opts agents.ApplyOpts) ([]agents.FileAction, error) {
out := make([]agents.FileAction, 0, len(SubAgents))
dir := filepath.Join(home, ".claude", "agents")
dir := filepath.Join(userClaudeConfigDir(home), "agents")
for name, content := range SubAgents {
path := filepath.Join(dir, name)
action, err := agents.WriteIfNotExists(w, path, content, opts)
Expand Down Expand Up @@ -404,26 +405,40 @@ func upsertGlobalMCPConfig(w io.Writer, path string, opts agents.ApplyOpts) (age
// Paths — user-level files.

func userClaudeJSONPath(home string) string {
if dir := claudeConfigDirOverride(); dir != "" {
return filepath.Join(dir, ".claude.json")
}
return filepath.Join(home, ".claude.json")
}

func userSettingsLocalPath(home string) string {
return filepath.Join(home, ".claude", "settings.local.json")
return filepath.Join(userClaudeConfigDir(home), "settings.local.json")
}

// userSettingsPath is the user-level counterpart to
// `.claude/settings.json` in a project. Permissions live here (not
// in settings.local.json) so they survive when the user wipes the
// "local" overrides file.
func userSettingsPath(home string) string {
return filepath.Join(home, ".claude", "settings.json")
return filepath.Join(userClaudeConfigDir(home), "settings.json")
}

// userClaudeMdPath is the machine-wide CLAUDE.md Claude Code reads on
// every session, regardless of cwd. We merge a marker-fenced rule
// block into it so the agent sees the Gortex rules from turn one.
func userClaudeMdPath(home string) string {
return filepath.Join(home, ".claude", "CLAUDE.md")
return filepath.Join(userClaudeConfigDir(home), "CLAUDE.md")
}

func userClaudeConfigDir(home string) string {
if dir := claudeConfigDirOverride(); dir != "" {
return dir
}
return filepath.Join(home, ".claude")
}

func claudeConfigDirOverride() string {
return strings.TrimSpace(os.Getenv("CLAUDE_CONFIG_DIR"))
}

func logf(w io.Writer, format string, args ...any) {
Expand Down
57 changes: 57 additions & 0 deletions internal/agents/claudecode/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,63 @@ func TestClaudeCodeGlobalModeWritesUserFiles(t *testing.T) {
}
}

func TestClaudeCodeGlobalModeHonorsClaudeConfigDir(t *testing.T) {
env, _ := agentstest.NewEnv(t)
env.Mode = agents.ModeGlobal
env.InstallGlobalInstructions = true
configDir := filepath.Join(t.TempDir(), "work-profile")
t.Setenv("CLAUDE_CONFIG_DIR", configDir)
a := New()

res, err := a.Apply(env, agents.ApplyOpts{})
if err != nil {
t.Fatalf("apply: %v", err)
}
if !res.Configured {
t.Fatal("expected Configured=true in global mode")
}

expected := []string{
filepath.Join(configDir, ".claude.json"),
filepath.Join(configDir, "settings.json"),
filepath.Join(configDir, "settings.local.json"),
filepath.Join(configDir, "CLAUDE.md"),
}
for name := range SlashCommands {
expected = append(expected, filepath.Join(configDir, "commands", name))
}
for name := range GlobalSkills {
expected = append(expected, filepath.Join(configDir, "skills", name, "SKILL.md"))
}
for name := range SubAgents {
expected = append(expected, filepath.Join(configDir, "agents", name))
}
for _, p := range expected {
if _, err := os.Stat(p); err != nil {
t.Errorf("missing CLAUDE_CONFIG_DIR artifact %s: %v", p, err)
}
}

for _, p := range []string{
filepath.Join(env.Home, ".claude.json"),
filepath.Join(env.Home, ".claude"),
} {
if _, err := os.Stat(p); err == nil {
t.Errorf("global mode unexpectedly wrote HOME artifact %s", p)
}
}

plan, err := a.Plan(env)
if err != nil {
t.Fatalf("plan: %v", err)
}
for _, f := range plan.Files {
if !strings.HasPrefix(f.Path, configDir+string(os.PathSeparator)) {
t.Errorf("planned path %s did not use CLAUDE_CONFIG_DIR %s", f.Path, configDir)
}
}
}

// TestClaudeCodeGlobalMode_NoClaudeMd skips the rule block when the
// caller opts out via InstallGlobalInstructions=false (i.e.
// `gortex install --no-claude-md`).
Expand Down