-
Notifications
You must be signed in to change notification settings - Fork 0
fix: respect CLAUDE_CONFIG_DIR env var for Claude config directory #326
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package claudeconfig | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| ) | ||
|
|
||
| // GetClaudeConfigDir returns the Claude Code configuration directory. | ||
| // If the CLAUDE_CONFIG_DIR environment variable is set, it is used as-is. | ||
| // Otherwise, falls back to ~/.claude. | ||
| func GetClaudeConfigDir() (string, error) { | ||
| if dir := os.Getenv("CLAUDE_CONFIG_DIR"); dir != "" { | ||
| return dir, nil | ||
| } | ||
| home, err := os.UserHomeDir() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| return filepath.Join(home, ".claude"), nil | ||
|
Comment on lines
+11
to
+19
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package claudeconfig | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestGetClaudeConfigDir(t *testing.T) { | ||
| home, err := os.UserHomeDir() | ||
| if err != nil { | ||
| t.Fatalf("failed to get home dir: %v", err) | ||
| } | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| envVal string | ||
| want string | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "env var set returns env var value", | ||
| envVal: "/custom/claude/config", | ||
| want: "/custom/claude/config", | ||
| }, | ||
| { | ||
| name: "env var unset returns ~/.claude", | ||
| envVal: "", | ||
| want: filepath.Join(home, ".claude"), | ||
| }, | ||
| { | ||
| name: "env var with trailing slash is returned as-is", | ||
| envVal: "/custom/dir/", | ||
| want: "/custom/dir/", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Setenv("CLAUDE_CONFIG_DIR", tt.envVal) | ||
|
|
||
| got, err := GetClaudeConfigDir() | ||
| if (err != nil) != tt.wantErr { | ||
| t.Fatalf("GetClaudeConfigDir() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| if got != tt.want { | ||
| t.Errorf("GetClaudeConfigDir() = %q, want %q", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -986,6 +986,45 @@ func TestReadPlanFromPath(t *testing.T) { | |
| }) | ||
| } | ||
|
|
||
| func TestValidatePlanPath_ClaudeConfigDir(t *testing.T) { | ||
| // When CLAUDE_CONFIG_DIR is set, plans must be under that directory. | ||
| customDir := t.TempDir() | ||
| t.Setenv("CLAUDE_CONFIG_DIR", customDir) | ||
|
|
||
| plansDir := filepath.Join(customDir, "plans") | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| path string | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "valid path in custom plans directory", | ||
| path: filepath.Join(plansDir, "my-plan.md"), | ||
| wantErr: false, | ||
| }, | ||
| { | ||
| name: "path in default ~/.claude/plans is rejected when CLAUDE_CONFIG_DIR is set", | ||
| path: filepath.Join(os.Getenv("HOME"), ".claude", "plans", "plan.md"), | ||
| wantErr: true, | ||
| }, | ||
|
Comment on lines
+1007
to
+1010
|
||
| { | ||
| name: "path traversal still rejected", | ||
| path: filepath.Join(plansDir, "..", "..", "etc", "passwd"), | ||
| wantErr: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := validatePlanPath(tt.path) | ||
| if (err != nil) != tt.wantErr { | ||
| t.Errorf("validatePlanPath(%q) error = %v, wantErr %v", tt.path, err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestValidatePlanPath(t *testing.T) { | ||
| homeDir, err := os.UserHomeDir() | ||
| if err != nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import ( | |
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/zhubert/plural/internal/claudeconfig" | ||
| "github.com/zhubert/plural/internal/logger" | ||
| ) | ||
|
|
||
|
|
@@ -33,11 +34,11 @@ type Plugin struct { | |
|
|
||
| // getClaudeDir returns the Claude config directory path | ||
| func getClaudeDir() string { | ||
| home, err := os.UserHomeDir() | ||
| dir, err := claudeconfig.GetClaudeConfigDir() | ||
| if err != nil { | ||
| return "" | ||
| } | ||
| return filepath.Join(home, ".claude") | ||
| return dir | ||
| } | ||
|
Comment on lines
35
to
42
|
||
|
|
||
| // knownMarketplacesFile is the structure of ~/.claude/plugins/known_marketplaces.json | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buildContainerRunArgs() now mounts the host Claude config dir directly. If CLAUDE_CONFIG_DIR is set to a relative path (or includes whitespace), docker bind mounts may fail at runtime (bind mounts generally require absolute host paths) and the failure won't be caught here. Consider normalizing/validating the resolved directory (e.g., ensure absolute) before constructing the -v argument and returning a clear error if it's invalid.