mirror of
https://github.com/cexll/myclaude.git
synced 2026-02-11 03:23:50 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13465b12e5 | ||
|
|
cf93a0ada9 | ||
|
|
b81953a1d7 | ||
|
|
1d2f28101a |
22
README.md
22
README.md
@@ -536,6 +536,28 @@ network_access = true
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Q5: Permission denied or sandbox restrictions during execution
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
Execution fails with permission errors or sandbox restrictions when running codeagent-wrapper.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
Set the following environment variables:
|
||||||
|
```bash
|
||||||
|
export CODEX_BYPASS_SANDBOX=true
|
||||||
|
export CODEAGENT_SKIP_PERMISSIONS=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Or add them to your shell profile (`~/.zshrc` or `~/.bashrc`):
|
||||||
|
```bash
|
||||||
|
echo 'export CODEX_BYPASS_SANDBOX=true' >> ~/.zshrc
|
||||||
|
echo 'export CODEAGENT_SKIP_PERMISSIONS=true' >> ~/.zshrc
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** These settings bypass security restrictions. Use with caution in trusted environments only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**Still having issues?** Visit [GitHub Issues](https://github.com/cexll/myclaude/issues) to search or report new issues.
|
**Still having issues?** Visit [GitHub Issues](https://github.com/cexll/myclaude/issues) to search or report new issues.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
22
README_CN.md
22
README_CN.md
@@ -408,6 +408,28 @@ network_access = true
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Q5: 执行时遇到权限拒绝或沙箱限制
|
||||||
|
|
||||||
|
**问题描述:**
|
||||||
|
运行 codeagent-wrapper 时出现权限错误或沙箱限制。
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
设置以下环境变量:
|
||||||
|
```bash
|
||||||
|
export CODEX_BYPASS_SANDBOX=true
|
||||||
|
export CODEAGENT_SKIP_PERMISSIONS=true
|
||||||
|
```
|
||||||
|
|
||||||
|
或添加到 shell 配置文件(`~/.zshrc` 或 `~/.bashrc`):
|
||||||
|
```bash
|
||||||
|
echo 'export CODEX_BYPASS_SANDBOX=true' >> ~/.zshrc
|
||||||
|
echo 'export CODEAGENT_SKIP_PERMISSIONS=true' >> ~/.zshrc
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意:** 这些设置会绕过安全限制,请仅在可信环境中使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**仍有疑问?** 请访问 [GitHub Issues](https://github.com/cexll/myclaude/issues) 搜索或提交新问题。
|
**仍有疑问?** 请访问 [GitHub Issues](https://github.com/cexll/myclaude/issues) 搜索或提交新问题。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Backend defines the contract for invoking different AI CLI backends.
|
// Backend defines the contract for invoking different AI CLI backends.
|
||||||
@@ -37,33 +38,48 @@ func (ClaudeBackend) BuildArgs(cfg *Config, targetArg string) []string {
|
|||||||
|
|
||||||
const maxClaudeSettingsBytes = 1 << 20 // 1MB
|
const maxClaudeSettingsBytes = 1 << 20 // 1MB
|
||||||
|
|
||||||
// loadMinimalEnvSettings 从 ~/.claude/settings.json 只提取 env 配置。
|
type minimalClaudeSettings struct {
|
||||||
// 只接受字符串类型的值;文件缺失/解析失败/超限都返回空。
|
Env map[string]string
|
||||||
func loadMinimalEnvSettings() map[string]string {
|
Model string
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadMinimalClaudeSettings 从 ~/.claude/settings.json 只提取安全的最小子集:
|
||||||
|
// - env: 只接受字符串类型的值
|
||||||
|
// - model: 只接受字符串类型的值
|
||||||
|
// 文件缺失/解析失败/超限都返回空。
|
||||||
|
func loadMinimalClaudeSettings() minimalClaudeSettings {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil || home == "" {
|
if err != nil || home == "" {
|
||||||
return nil
|
return minimalClaudeSettings{}
|
||||||
}
|
}
|
||||||
|
|
||||||
settingPath := filepath.Join(home, ".claude", "settings.json")
|
settingPath := filepath.Join(home, ".claude", "settings.json")
|
||||||
info, err := os.Stat(settingPath)
|
info, err := os.Stat(settingPath)
|
||||||
if err != nil || info.Size() > maxClaudeSettingsBytes {
|
if err != nil || info.Size() > maxClaudeSettingsBytes {
|
||||||
return nil
|
return minimalClaudeSettings{}
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(settingPath)
|
data, err := os.ReadFile(settingPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return minimalClaudeSettings{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Env map[string]any `json:"env"`
|
Env map[string]any `json:"env"`
|
||||||
|
Model any `json:"model"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
return nil
|
return minimalClaudeSettings{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out := minimalClaudeSettings{}
|
||||||
|
|
||||||
|
if model, ok := cfg.Model.(string); ok {
|
||||||
|
out.Model = strings.TrimSpace(model)
|
||||||
|
}
|
||||||
|
|
||||||
if len(cfg.Env) == 0 {
|
if len(cfg.Env) == 0 {
|
||||||
return nil
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
env := make(map[string]string, len(cfg.Env))
|
env := make(map[string]string, len(cfg.Env))
|
||||||
@@ -75,9 +91,19 @@ func loadMinimalEnvSettings() map[string]string {
|
|||||||
env[k] = s
|
env[k] = s
|
||||||
}
|
}
|
||||||
if len(env) == 0 {
|
if len(env) == 0 {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
out.Env = env
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadMinimalEnvSettings is kept for backwards tests; prefer loadMinimalClaudeSettings.
|
||||||
|
func loadMinimalEnvSettings() map[string]string {
|
||||||
|
settings := loadMinimalClaudeSettings()
|
||||||
|
if len(settings.Env) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return env
|
return settings.Env
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildClaudeArgs(cfg *Config, targetArg string) []string {
|
func buildClaudeArgs(cfg *Config, targetArg string) []string {
|
||||||
@@ -93,6 +119,10 @@ func buildClaudeArgs(cfg *Config, targetArg string) []string {
|
|||||||
// This ensures a clean execution environment without CLAUDE.md or skills that would trigger codeagent
|
// This ensures a clean execution environment without CLAUDE.md or skills that would trigger codeagent
|
||||||
args = append(args, "--setting-sources", "")
|
args = append(args, "--setting-sources", "")
|
||||||
|
|
||||||
|
if model := strings.TrimSpace(cfg.Model); model != "" {
|
||||||
|
args = append(args, "--model", model)
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Mode == "resume" {
|
if cfg.Mode == "resume" {
|
||||||
if cfg.SessionID != "" {
|
if cfg.SessionID != "" {
|
||||||
// Claude CLI uses -r <session_id> for resume.
|
// Claude CLI uses -r <session_id> for resume.
|
||||||
@@ -122,6 +152,10 @@ func buildGeminiArgs(cfg *Config, targetArg string) []string {
|
|||||||
}
|
}
|
||||||
args := []string{"-o", "stream-json", "-y"}
|
args := []string{"-o", "stream-json", "-y"}
|
||||||
|
|
||||||
|
if model := strings.TrimSpace(cfg.Model); model != "" {
|
||||||
|
args = append(args, "-m", model)
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Mode == "resume" {
|
if cfg.Mode == "resume" {
|
||||||
if cfg.SessionID != "" {
|
if cfg.SessionID != "" {
|
||||||
args = append(args, "-r", cfg.SessionID)
|
args = append(args, "-r", cfg.SessionID)
|
||||||
|
|||||||
@@ -63,6 +63,42 @@ func TestClaudeBuildArgs_ModesAndPermissions(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackendBuildArgs_Model(t *testing.T) {
|
||||||
|
t.Run("claude includes --model when set", func(t *testing.T) {
|
||||||
|
backend := ClaudeBackend{}
|
||||||
|
cfg := &Config{Mode: "new", Model: "opus"}
|
||||||
|
got := backend.BuildArgs(cfg, "todo")
|
||||||
|
want := []string{"-p", "--setting-sources", "", "--model", "opus", "--output-format", "stream-json", "--verbose", "todo"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gemini includes -m when set", func(t *testing.T) {
|
||||||
|
backend := GeminiBackend{}
|
||||||
|
cfg := &Config{Mode: "new", Model: "gemini-3-pro-preview"}
|
||||||
|
got := backend.BuildArgs(cfg, "task")
|
||||||
|
want := []string{"-o", "stream-json", "-y", "-m", "gemini-3-pro-preview", "-p", "task"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("codex includes --model when set", func(t *testing.T) {
|
||||||
|
const key = "CODEX_BYPASS_SANDBOX"
|
||||||
|
t.Cleanup(func() { os.Unsetenv(key) })
|
||||||
|
os.Unsetenv(key)
|
||||||
|
|
||||||
|
backend := CodexBackend{}
|
||||||
|
cfg := &Config{Mode: "new", WorkDir: "/tmp", Model: "o3"}
|
||||||
|
got := backend.BuildArgs(cfg, "task")
|
||||||
|
want := []string{"e", "--model", "o3", "--skip-git-repo-check", "-C", "/tmp", "--json", "task"}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) {
|
func TestClaudeBuildArgs_GeminiAndCodexModes(t *testing.T) {
|
||||||
t.Run("gemini new mode defaults workdir", func(t *testing.T) {
|
t.Run("gemini new mode defaults workdir", func(t *testing.T) {
|
||||||
backend := GeminiBackend{}
|
backend := GeminiBackend{}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Config struct {
|
|||||||
Task string
|
Task string
|
||||||
SessionID string
|
SessionID string
|
||||||
WorkDir string
|
WorkDir string
|
||||||
|
Model string
|
||||||
ExplicitStdin bool
|
ExplicitStdin bool
|
||||||
Timeout int
|
Timeout int
|
||||||
Backend string
|
Backend string
|
||||||
@@ -36,6 +37,7 @@ type TaskSpec struct {
|
|||||||
Dependencies []string `json:"dependencies,omitempty"`
|
Dependencies []string `json:"dependencies,omitempty"`
|
||||||
SessionID string `json:"session_id,omitempty"`
|
SessionID string `json:"session_id,omitempty"`
|
||||||
Backend string `json:"backend,omitempty"`
|
Backend string `json:"backend,omitempty"`
|
||||||
|
Model string `json:"model,omitempty"`
|
||||||
Mode string `json:"-"`
|
Mode string `json:"-"`
|
||||||
UseStdin bool `json:"-"`
|
UseStdin bool `json:"-"`
|
||||||
Context context.Context `json:"-"`
|
Context context.Context `json:"-"`
|
||||||
@@ -152,6 +154,8 @@ func parseParallelConfig(data []byte) (*ParallelConfig, error) {
|
|||||||
task.Mode = "resume"
|
task.Mode = "resume"
|
||||||
case "backend":
|
case "backend":
|
||||||
task.Backend = value
|
task.Backend = value
|
||||||
|
case "model":
|
||||||
|
task.Model = value
|
||||||
case "dependencies":
|
case "dependencies":
|
||||||
for _, dep := range strings.Split(value, ",") {
|
for _, dep := range strings.Split(value, ",") {
|
||||||
dep = strings.TrimSpace(dep)
|
dep = strings.TrimSpace(dep)
|
||||||
@@ -198,6 +202,7 @@ func parseArgs() (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backendName := defaultBackendName
|
backendName := defaultBackendName
|
||||||
|
model := ""
|
||||||
skipPermissions := envFlagEnabled("CODEAGENT_SKIP_PERMISSIONS")
|
skipPermissions := envFlagEnabled("CODEAGENT_SKIP_PERMISSIONS")
|
||||||
filtered := make([]string, 0, len(args))
|
filtered := make([]string, 0, len(args))
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
@@ -220,6 +225,20 @@ func parseArgs() (*Config, error) {
|
|||||||
case arg == "--skip-permissions", arg == "--dangerously-skip-permissions":
|
case arg == "--skip-permissions", arg == "--dangerously-skip-permissions":
|
||||||
skipPermissions = true
|
skipPermissions = true
|
||||||
continue
|
continue
|
||||||
|
case arg == "--model":
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
return nil, fmt.Errorf("--model flag requires a value")
|
||||||
|
}
|
||||||
|
model = args[i+1]
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
case strings.HasPrefix(arg, "--model="):
|
||||||
|
value := strings.TrimPrefix(arg, "--model=")
|
||||||
|
if value == "" {
|
||||||
|
return nil, fmt.Errorf("--model flag requires a value")
|
||||||
|
}
|
||||||
|
model = value
|
||||||
|
continue
|
||||||
case strings.HasPrefix(arg, "--skip-permissions="):
|
case strings.HasPrefix(arg, "--skip-permissions="):
|
||||||
skipPermissions = parseBoolFlag(strings.TrimPrefix(arg, "--skip-permissions="), skipPermissions)
|
skipPermissions = parseBoolFlag(strings.TrimPrefix(arg, "--skip-permissions="), skipPermissions)
|
||||||
continue
|
continue
|
||||||
@@ -235,7 +254,7 @@ func parseArgs() (*Config, error) {
|
|||||||
}
|
}
|
||||||
args = filtered
|
args = filtered
|
||||||
|
|
||||||
cfg := &Config{WorkDir: defaultWorkdir, Backend: backendName, SkipPermissions: skipPermissions}
|
cfg := &Config{WorkDir: defaultWorkdir, Backend: backendName, SkipPermissions: skipPermissions, Model: strings.TrimSpace(model)}
|
||||||
cfg.MaxParallelWorkers = resolveMaxParallelWorkers()
|
cfg.MaxParallelWorkers = resolveMaxParallelWorkers()
|
||||||
|
|
||||||
if args[0] == "resume" {
|
if args[0] == "resume" {
|
||||||
|
|||||||
@@ -744,6 +744,10 @@ func buildCodexArgs(cfg *Config, targetArg string) []string {
|
|||||||
args = append(args, "--dangerously-bypass-approvals-and-sandbox")
|
args = append(args, "--dangerously-bypass-approvals-and-sandbox")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if model := strings.TrimSpace(cfg.Model); model != "" {
|
||||||
|
args = append(args, "--model", model)
|
||||||
|
}
|
||||||
|
|
||||||
args = append(args, "--skip-git-repo-check")
|
args = append(args, "--skip-git-repo-check")
|
||||||
|
|
||||||
if isResume {
|
if isResume {
|
||||||
@@ -788,6 +792,7 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
|||||||
Task: taskSpec.Task,
|
Task: taskSpec.Task,
|
||||||
SessionID: taskSpec.SessionID,
|
SessionID: taskSpec.SessionID,
|
||||||
WorkDir: taskSpec.WorkDir,
|
WorkDir: taskSpec.WorkDir,
|
||||||
|
Model: taskSpec.Model,
|
||||||
Backend: defaultBackendName,
|
Backend: defaultBackendName,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,6 +821,15 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var claudeEnv map[string]string
|
||||||
|
if cfg.Backend == "claude" {
|
||||||
|
settings := loadMinimalClaudeSettings()
|
||||||
|
claudeEnv = settings.Env
|
||||||
|
if cfg.Mode != "resume" && strings.TrimSpace(cfg.Model) == "" && settings.Model != "" {
|
||||||
|
cfg.Model = settings.Model
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useStdin := taskSpec.UseStdin
|
useStdin := taskSpec.UseStdin
|
||||||
targetArg := taskSpec.Task
|
targetArg := taskSpec.Task
|
||||||
if useStdin {
|
if useStdin {
|
||||||
@@ -915,10 +929,8 @@ func runCodexTaskWithContext(parentCtx context.Context, taskSpec TaskSpec, backe
|
|||||||
|
|
||||||
cmd := newCommandRunner(ctx, commandName, codexArgs...)
|
cmd := newCommandRunner(ctx, commandName, codexArgs...)
|
||||||
|
|
||||||
if cfg.Backend == "claude" {
|
if cfg.Backend == "claude" && len(claudeEnv) > 0 {
|
||||||
if env := loadMinimalEnvSettings(); len(env) > 0 {
|
cmd.SetEnv(claudeEnv)
|
||||||
cmd.SetEnv(env)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For backends that don't support -C flag (claude, gemini), set working directory via cmd.Dir
|
// For backends that don't support -C flag (claude, gemini), set working directory via cmd.Dir
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ func run() (exitCode int) {
|
|||||||
|
|
||||||
if parallelIndex != -1 {
|
if parallelIndex != -1 {
|
||||||
backendName := defaultBackendName
|
backendName := defaultBackendName
|
||||||
|
model := ""
|
||||||
fullOutput := false
|
fullOutput := false
|
||||||
var extras []string
|
var extras []string
|
||||||
|
|
||||||
@@ -202,13 +203,27 @@ func run() (exitCode int) {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
backendName = value
|
backendName = value
|
||||||
|
case arg == "--model":
|
||||||
|
if i+1 >= len(args) {
|
||||||
|
fmt.Fprintln(os.Stderr, "ERROR: --model flag requires a value")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
model = args[i+1]
|
||||||
|
i++
|
||||||
|
case strings.HasPrefix(arg, "--model="):
|
||||||
|
value := strings.TrimPrefix(arg, "--model=")
|
||||||
|
if value == "" {
|
||||||
|
fmt.Fprintln(os.Stderr, "ERROR: --model flag requires a value")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
model = value
|
||||||
default:
|
default:
|
||||||
extras = append(extras, arg)
|
extras = append(extras, arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(extras) > 0 {
|
if len(extras) > 0 {
|
||||||
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin; only --backend and --full-output are allowed.")
|
fmt.Fprintln(os.Stderr, "ERROR: --parallel reads its task configuration from stdin; only --backend, --model and --full-output are allowed.")
|
||||||
fmt.Fprintln(os.Stderr, "Usage examples:")
|
fmt.Fprintln(os.Stderr, "Usage examples:")
|
||||||
fmt.Fprintf(os.Stderr, " %s --parallel < tasks.txt\n", name)
|
fmt.Fprintf(os.Stderr, " %s --parallel < tasks.txt\n", name)
|
||||||
fmt.Fprintf(os.Stderr, " echo '...' | %s --parallel\n", name)
|
fmt.Fprintf(os.Stderr, " echo '...' | %s --parallel\n", name)
|
||||||
@@ -237,10 +252,14 @@ func run() (exitCode int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg.GlobalBackend = backendName
|
cfg.GlobalBackend = backendName
|
||||||
|
model = strings.TrimSpace(model)
|
||||||
for i := range cfg.Tasks {
|
for i := range cfg.Tasks {
|
||||||
if strings.TrimSpace(cfg.Tasks[i].Backend) == "" {
|
if strings.TrimSpace(cfg.Tasks[i].Backend) == "" {
|
||||||
cfg.Tasks[i].Backend = backendName
|
cfg.Tasks[i].Backend = backendName
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(cfg.Tasks[i].Model) == "" && model != "" {
|
||||||
|
cfg.Tasks[i].Model = model
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timeoutSec := resolveTimeout()
|
timeoutSec := resolveTimeout()
|
||||||
@@ -409,6 +428,7 @@ func run() (exitCode int) {
|
|||||||
WorkDir: cfg.WorkDir,
|
WorkDir: cfg.WorkDir,
|
||||||
Mode: cfg.Mode,
|
Mode: cfg.Mode,
|
||||||
SessionID: cfg.SessionID,
|
SessionID: cfg.SessionID,
|
||||||
|
Model: cfg.Model,
|
||||||
UseStdin: useStdin,
|
UseStdin: useStdin,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1139,6 +1139,65 @@ func TestBackendParseArgs_BackendFlag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackendParseArgs_ModelFlag(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "model flag",
|
||||||
|
args: []string{"codeagent-wrapper", "--model", "opus", "task"},
|
||||||
|
want: "opus",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "model equals syntax",
|
||||||
|
args: []string{"codeagent-wrapper", "--model=opus", "task"},
|
||||||
|
want: "opus",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "model trimmed",
|
||||||
|
args: []string{"codeagent-wrapper", "--model", " opus ", "task"},
|
||||||
|
want: "opus",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "model with resume mode",
|
||||||
|
args: []string{"codeagent-wrapper", "--model", "sonnet", "resume", "sid", "task"},
|
||||||
|
want: "sonnet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing model value",
|
||||||
|
args: []string{"codeagent-wrapper", "--model"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "model equals missing value",
|
||||||
|
args: []string{"codeagent-wrapper", "--model=", "task"},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
os.Args = tt.args
|
||||||
|
cfg, err := parseArgs()
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Model != tt.want {
|
||||||
|
t.Fatalf("Model = %q, want %q", cfg.Model, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBackendParseArgs_SkipPermissions(t *testing.T) {
|
func TestBackendParseArgs_SkipPermissions(t *testing.T) {
|
||||||
const envKey = "CODEAGENT_SKIP_PERMISSIONS"
|
const envKey = "CODEAGENT_SKIP_PERMISSIONS"
|
||||||
t.Cleanup(func() { os.Unsetenv(envKey) })
|
t.Cleanup(func() { os.Unsetenv(envKey) })
|
||||||
@@ -1276,6 +1335,26 @@ do something`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParallelParseConfig_Model(t *testing.T) {
|
||||||
|
input := `---TASK---
|
||||||
|
id: task-1
|
||||||
|
model: opus
|
||||||
|
---CONTENT---
|
||||||
|
do something`
|
||||||
|
|
||||||
|
cfg, err := parseParallelConfig([]byte(input))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseParallelConfig() unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(cfg.Tasks) != 1 {
|
||||||
|
t.Fatalf("expected 1 task, got %d", len(cfg.Tasks))
|
||||||
|
}
|
||||||
|
task := cfg.Tasks[0]
|
||||||
|
if task.Model != "opus" {
|
||||||
|
t.Fatalf("model = %q, want opus", task.Model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParallelParseConfig_EmptySessionID(t *testing.T) {
|
func TestParallelParseConfig_EmptySessionID(t *testing.T) {
|
||||||
input := `---TASK---
|
input := `---TASK---
|
||||||
id: task-1
|
id: task-1
|
||||||
@@ -1358,6 +1437,120 @@ code with special chars: $var "quotes"`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClaudeModel_DefaultsFromSettings(t *testing.T) {
|
||||||
|
defer resetTestHooks()
|
||||||
|
|
||||||
|
home := t.TempDir()
|
||||||
|
t.Setenv("HOME", home)
|
||||||
|
t.Setenv("USERPROFILE", home)
|
||||||
|
|
||||||
|
dir := filepath.Join(home, ".claude")
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
t.Fatalf("MkdirAll: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsModel := "claude-opus-4-5-20250929"
|
||||||
|
path := filepath.Join(dir, "settings.json")
|
||||||
|
data := []byte(fmt.Sprintf(`{"model":%q,"env":{"FOO":"bar"}}`, settingsModel))
|
||||||
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeRunner := func(gotName *string, gotArgs *[]string, fake **fakeCmd) func(context.Context, string, ...string) commandRunner {
|
||||||
|
return func(ctx context.Context, name string, args ...string) commandRunner {
|
||||||
|
*gotName = name
|
||||||
|
*gotArgs = append([]string(nil), args...)
|
||||||
|
cmd := newFakeCmd(fakeCmdConfig{
|
||||||
|
PID: 123,
|
||||||
|
StdoutPlan: []fakeStdoutEvent{
|
||||||
|
{Data: "{\"type\":\"result\",\"session_id\":\"sid\",\"result\":\"ok\"}\n"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
*fake = cmd
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("new mode inherits model when unset", func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
gotName string
|
||||||
|
gotArgs []string
|
||||||
|
fake *fakeCmd
|
||||||
|
)
|
||||||
|
origRunner := newCommandRunner
|
||||||
|
newCommandRunner = makeRunner(&gotName, &gotArgs, &fake)
|
||||||
|
t.Cleanup(func() { newCommandRunner = origRunner })
|
||||||
|
|
||||||
|
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "hi", Mode: "new", WorkDir: defaultWorkdir}, ClaudeBackend{}, nil, false, true, 5)
|
||||||
|
if res.ExitCode != 0 || res.Message != "ok" {
|
||||||
|
t.Fatalf("unexpected result: %+v", res)
|
||||||
|
}
|
||||||
|
if gotName != "claude" {
|
||||||
|
t.Fatalf("command = %q, want claude", gotName)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for i := 0; i+1 < len(gotArgs); i++ {
|
||||||
|
if gotArgs[i] == "--model" && gotArgs[i+1] == settingsModel {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("expected --model %q in args, got %v", settingsModel, gotArgs)
|
||||||
|
}
|
||||||
|
if fake == nil || fake.env["FOO"] != "bar" {
|
||||||
|
t.Fatalf("expected env to include FOO=bar, got %v", fake.env)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("explicit model overrides settings", func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
gotName string
|
||||||
|
gotArgs []string
|
||||||
|
fake *fakeCmd
|
||||||
|
)
|
||||||
|
origRunner := newCommandRunner
|
||||||
|
newCommandRunner = makeRunner(&gotName, &gotArgs, &fake)
|
||||||
|
t.Cleanup(func() { newCommandRunner = origRunner })
|
||||||
|
|
||||||
|
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "hi", Mode: "new", WorkDir: defaultWorkdir, Model: "sonnet"}, ClaudeBackend{}, nil, false, true, 5)
|
||||||
|
if res.ExitCode != 0 || res.Message != "ok" {
|
||||||
|
t.Fatalf("unexpected result: %+v", res)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for i := 0; i+1 < len(gotArgs); i++ {
|
||||||
|
if gotArgs[i] == "--model" && gotArgs[i+1] == "sonnet" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("expected --model sonnet in args, got %v", gotArgs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("resume mode does not inherit model by default", func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
gotName string
|
||||||
|
gotArgs []string
|
||||||
|
fake *fakeCmd
|
||||||
|
)
|
||||||
|
origRunner := newCommandRunner
|
||||||
|
newCommandRunner = makeRunner(&gotName, &gotArgs, &fake)
|
||||||
|
t.Cleanup(func() { newCommandRunner = origRunner })
|
||||||
|
|
||||||
|
res := runCodexTaskWithContext(context.Background(), TaskSpec{Task: "hi", Mode: "resume", SessionID: "sid-123", WorkDir: defaultWorkdir}, ClaudeBackend{}, nil, false, true, 5)
|
||||||
|
if res.ExitCode != 0 || res.Message != "ok" {
|
||||||
|
t.Fatalf("unexpected result: %+v", res)
|
||||||
|
}
|
||||||
|
for i := 0; i < len(gotArgs); i++ {
|
||||||
|
if gotArgs[i] == "--model" {
|
||||||
|
t.Fatalf("did not expect --model in resume args, got %v", gotArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunShouldUseStdin(t *testing.T) {
|
func TestRunShouldUseStdin(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -2947,6 +3140,50 @@ do two`)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParallelModelPropagation(t *testing.T) {
|
||||||
|
defer resetTestHooks()
|
||||||
|
cleanupLogsFn = func() (CleanupStats, error) { return CleanupStats{}, nil }
|
||||||
|
|
||||||
|
orig := runCodexTaskFn
|
||||||
|
var mu sync.Mutex
|
||||||
|
seen := make(map[string]string)
|
||||||
|
runCodexTaskFn = func(task TaskSpec, timeout int) TaskResult {
|
||||||
|
mu.Lock()
|
||||||
|
seen[task.ID] = task.Model
|
||||||
|
mu.Unlock()
|
||||||
|
return TaskResult{TaskID: task.ID, ExitCode: 0, Message: "ok"}
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { runCodexTaskFn = orig })
|
||||||
|
|
||||||
|
stdinReader = strings.NewReader(`---TASK---
|
||||||
|
id: first
|
||||||
|
---CONTENT---
|
||||||
|
do one
|
||||||
|
|
||||||
|
---TASK---
|
||||||
|
id: second
|
||||||
|
model: opus
|
||||||
|
---CONTENT---
|
||||||
|
do two`)
|
||||||
|
os.Args = []string{"codeagent-wrapper", "--parallel", "--model", "sonnet"}
|
||||||
|
|
||||||
|
if code := run(); code != 0 {
|
||||||
|
t.Fatalf("run exit = %d, want 0", code)
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
firstModel, firstOK := seen["first"]
|
||||||
|
secondModel, secondOK := seen["second"]
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if !firstOK || firstModel != "sonnet" {
|
||||||
|
t.Fatalf("first model = %q (present=%v), want sonnet", firstModel, firstOK)
|
||||||
|
}
|
||||||
|
if !secondOK || secondModel != "opus" {
|
||||||
|
t.Fatalf("second model = %q (present=%v), want opus", secondModel, secondOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParallelFlag(t *testing.T) {
|
func TestParallelFlag(t *testing.T) {
|
||||||
oldArgs := os.Args
|
oldArgs := os.Args
|
||||||
defer func() { os.Args = oldArgs }()
|
defer func() { os.Args = oldArgs }()
|
||||||
|
|||||||
167
skills/skill-install/SKILL.md
Normal file
167
skills/skill-install/SKILL.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
---
|
||||||
|
name: skill-install
|
||||||
|
description: Install Claude skills from GitHub repositories with automated security scanning. Triggers when users want to install skills from a GitHub URL, need to browse available skills in a repository, or want to safely add new skills to their Claude environment.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill Install
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Install Claude skills from GitHub repositories with built-in security scanning to protect against malicious code, backdoors, and vulnerabilities.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
Trigger this skill when the user:
|
||||||
|
- Provides a GitHub repository URL and wants to install skills
|
||||||
|
- Asks to "install skills from GitHub"
|
||||||
|
- Wants to browse and select skills from a repository
|
||||||
|
- Needs to add new skills to their Claude environment
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Step 1: Parse GitHub URL
|
||||||
|
|
||||||
|
Accept a GitHub repository URL from the user. The URL should point to a repository containing a `skills/` directory.
|
||||||
|
|
||||||
|
Supported URL formats:
|
||||||
|
- `https://github.com/user/repo`
|
||||||
|
- `https://github.com/user/repo/tree/main/skills`
|
||||||
|
- `https://github.com/user/repo/tree/branch-name/skills`
|
||||||
|
|
||||||
|
Extract:
|
||||||
|
- Repository owner
|
||||||
|
- Repository name
|
||||||
|
- Branch (default to `main` if not specified)
|
||||||
|
|
||||||
|
### Step 2: Fetch Skills List
|
||||||
|
|
||||||
|
Use the WebFetch tool to retrieve the skills directory listing from GitHub.
|
||||||
|
|
||||||
|
GitHub API endpoint pattern:
|
||||||
|
```
|
||||||
|
https://api.github.com/repos/{owner}/{repo}/contents/skills?ref={branch}
|
||||||
|
```
|
||||||
|
|
||||||
|
Parse the response to extract:
|
||||||
|
- Skill directory names
|
||||||
|
- Each skill should be a subdirectory containing a SKILL.md file
|
||||||
|
|
||||||
|
### Step 3: Present Skills to User
|
||||||
|
|
||||||
|
Use the AskUserQuestion tool to let the user select which skills to install.
|
||||||
|
|
||||||
|
Set `multiSelect: true` to allow multiple selections.
|
||||||
|
|
||||||
|
Present each skill with:
|
||||||
|
- Skill name (directory name)
|
||||||
|
- Brief description (if available from SKILL.md frontmatter)
|
||||||
|
|
||||||
|
### Step 4: Fetch Skill Content
|
||||||
|
|
||||||
|
For each selected skill, fetch all files in the skill directory:
|
||||||
|
|
||||||
|
1. Get the file tree for the skill directory
|
||||||
|
2. Download all files (SKILL.md, scripts/, references/, assets/)
|
||||||
|
3. Store the complete skill content for security analysis
|
||||||
|
|
||||||
|
Use WebFetch with GitHub API:
|
||||||
|
```
|
||||||
|
https://api.github.com/repos/{owner}/{repo}/contents/skills/{skill_name}?ref={branch}
|
||||||
|
```
|
||||||
|
|
||||||
|
For each file, fetch the raw content:
|
||||||
|
```
|
||||||
|
https://raw.githubusercontent.com/{owner}/{repo}/{branch}/skills/{skill_name}/{file_path}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Security Scan
|
||||||
|
|
||||||
|
**CRITICAL:** Before installation, perform a thorough security analysis of each skill.
|
||||||
|
|
||||||
|
Read the security scan prompt template from `references/security_scan_prompt.md` and apply it to analyze the skill content.
|
||||||
|
|
||||||
|
Examine for:
|
||||||
|
1. **Malicious Command Execution** - eval, exec, subprocess with shell=True
|
||||||
|
2. **Backdoor Detection** - obfuscated code, suspicious network requests
|
||||||
|
3. **Credential Theft** - accessing ~/.ssh, ~/.aws, environment variables
|
||||||
|
4. **Unauthorized Network Access** - external requests to suspicious domains
|
||||||
|
5. **File System Abuse** - destructive operations, unauthorized writes
|
||||||
|
6. **Privilege Escalation** - sudo attempts, system modifications
|
||||||
|
7. **Supply Chain Attacks** - suspicious package installations
|
||||||
|
|
||||||
|
Output the security analysis with:
|
||||||
|
- Security Status: SAFE / WARNING / DANGEROUS
|
||||||
|
- Risk Level: LOW / MEDIUM / HIGH / CRITICAL
|
||||||
|
- Detailed findings with file locations and severity
|
||||||
|
- Recommendation: APPROVE / APPROVE_WITH_WARNINGS / REJECT
|
||||||
|
|
||||||
|
### Step 6: User Decision
|
||||||
|
|
||||||
|
Based on the security scan results:
|
||||||
|
|
||||||
|
**If SAFE (APPROVE):**
|
||||||
|
- Proceed directly to installation
|
||||||
|
|
||||||
|
**If WARNING (APPROVE_WITH_WARNINGS):**
|
||||||
|
- Display the security warnings to the user
|
||||||
|
- Use AskUserQuestion to confirm: "Security warnings detected. Do you want to proceed with installation?"
|
||||||
|
- Options: "Yes, install anyway" / "No, skip this skill"
|
||||||
|
|
||||||
|
**If DANGEROUS (REJECT):**
|
||||||
|
- Display the critical security issues
|
||||||
|
- Refuse to install
|
||||||
|
- Explain why the skill is dangerous
|
||||||
|
- Do NOT provide an option to override for CRITICAL severity issues
|
||||||
|
|
||||||
|
### Step 7: Install Skills
|
||||||
|
|
||||||
|
For approved skills, install to `~/.claude/skills/`:
|
||||||
|
|
||||||
|
1. Create the skill directory: `~/.claude/skills/{skill_name}/`
|
||||||
|
2. Write all skill files maintaining the directory structure
|
||||||
|
3. Ensure proper file permissions (executable for scripts)
|
||||||
|
4. Verify SKILL.md exists and has valid frontmatter
|
||||||
|
|
||||||
|
Use the Write tool to create files.
|
||||||
|
|
||||||
|
### Step 8: Confirmation
|
||||||
|
|
||||||
|
After installation, provide a summary:
|
||||||
|
- List of successfully installed skills
|
||||||
|
- List of skipped skills (if any) with reasons
|
||||||
|
- Location: `~/.claude/skills/`
|
||||||
|
- Next steps: "The skills are now available. Restart Claude or use them directly."
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
**User:** "Install skills from https://github.com/example/claude-skills"
|
||||||
|
|
||||||
|
**Assistant:**
|
||||||
|
1. Fetches skills list from the repository
|
||||||
|
2. Presents available skills: "skill-a", "skill-b", "skill-c"
|
||||||
|
3. User selects "skill-a" and "skill-b"
|
||||||
|
4. Performs security scan on each skill
|
||||||
|
5. skill-a: SAFE - proceeds to install
|
||||||
|
6. skill-b: WARNING (makes HTTP request) - asks user for confirmation
|
||||||
|
7. Installs approved skills to ~/.claude/skills/
|
||||||
|
8. Confirms: "Successfully installed: skill-a, skill-b"
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- **Never skip security scanning** - Always analyze skills before installation
|
||||||
|
- **Be conservative** - When in doubt, flag as WARNING and let user decide
|
||||||
|
- **Critical issues are blocking** - CRITICAL severity findings cannot be overridden
|
||||||
|
- **Transparency** - Always show users what was found during security scans
|
||||||
|
- **Sandboxing** - Remind users that skills run with Claude's permissions
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### references/security_scan_prompt.md
|
||||||
|
|
||||||
|
Contains the detailed security analysis prompt template with:
|
||||||
|
- Complete list of security categories to check
|
||||||
|
- Output format requirements
|
||||||
|
- Example analyses for safe, suspicious, and dangerous skills
|
||||||
|
- Decision criteria for APPROVE/REJECT recommendations
|
||||||
|
|
||||||
|
Load this file when performing security scans to ensure comprehensive analysis.
|
||||||
137
skills/skill-install/references/security_scan_prompt.md
Normal file
137
skills/skill-install/references/security_scan_prompt.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Security Scan Prompt for Skills
|
||||||
|
|
||||||
|
Use this prompt template to analyze skill content for security vulnerabilities before installation.
|
||||||
|
|
||||||
|
## Prompt Template
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a security expert analyzing a Claude skill for potential security risks.
|
||||||
|
|
||||||
|
Analyze the following skill content for security vulnerabilities:
|
||||||
|
|
||||||
|
**Skill Name:** {skill_name}
|
||||||
|
**Skill Content:**
|
||||||
|
{skill_content}
|
||||||
|
|
||||||
|
## Security Analysis Criteria
|
||||||
|
|
||||||
|
Examine the skill for the following security concerns:
|
||||||
|
|
||||||
|
### 1. Malicious Command Execution
|
||||||
|
- Detect `eval()`, `exec()`, `subprocess` with `shell=True`
|
||||||
|
- Identify arbitrary code execution patterns
|
||||||
|
- Check for command injection vulnerabilities
|
||||||
|
|
||||||
|
### 2. Backdoor Detection
|
||||||
|
- Look for obfuscated code (base64, hex encoding)
|
||||||
|
- Identify suspicious network requests to unknown domains
|
||||||
|
- Detect file hash patterns matching known malware
|
||||||
|
- Check for hidden data exfiltration mechanisms
|
||||||
|
|
||||||
|
### 3. Credential Theft
|
||||||
|
- Detect attempts to access environment variables containing secrets
|
||||||
|
- Identify file operations on sensitive paths (~/.ssh, ~/.aws, ~/.netrc)
|
||||||
|
- Check for credential harvesting patterns
|
||||||
|
- Look for keylogging or clipboard monitoring
|
||||||
|
|
||||||
|
### 4. Unauthorized Network Access
|
||||||
|
- Identify external network requests
|
||||||
|
- Check for connections to suspicious domains (pastebin, ngrok, bit.ly, etc.)
|
||||||
|
- Detect data exfiltration via HTTP/HTTPS
|
||||||
|
- Look for reverse shell patterns
|
||||||
|
|
||||||
|
### 5. File System Abuse
|
||||||
|
- Detect destructive file operations (rm -rf, shutil.rmtree)
|
||||||
|
- Identify unauthorized file writes to system directories
|
||||||
|
- Check for file permission modifications
|
||||||
|
- Look for attempts to modify critical system files
|
||||||
|
|
||||||
|
### 6. Privilege Escalation
|
||||||
|
- Detect sudo or privilege escalation attempts
|
||||||
|
- Identify attempts to modify system configurations
|
||||||
|
- Check for container escape patterns
|
||||||
|
|
||||||
|
### 7. Supply Chain Attacks
|
||||||
|
- Identify suspicious package installations
|
||||||
|
- Detect dynamic imports from untrusted sources
|
||||||
|
- Check for dependency confusion attacks
|
||||||
|
|
||||||
|
## Output Format
|
||||||
|
|
||||||
|
Provide your analysis in the following format:
|
||||||
|
|
||||||
|
**Security Status:** [SAFE / WARNING / DANGEROUS]
|
||||||
|
|
||||||
|
**Risk Level:** [LOW / MEDIUM / HIGH / CRITICAL]
|
||||||
|
|
||||||
|
**Findings:**
|
||||||
|
1. [Category]: [Description]
|
||||||
|
- File: [filename:line_number]
|
||||||
|
- Severity: [LOW/MEDIUM/HIGH/CRITICAL]
|
||||||
|
- Details: [Explanation]
|
||||||
|
- Recommendation: [How to fix or mitigate]
|
||||||
|
|
||||||
|
**Summary:**
|
||||||
|
[Brief summary of the security assessment]
|
||||||
|
|
||||||
|
**Recommendation:**
|
||||||
|
[APPROVE / REJECT / APPROVE_WITH_WARNINGS]
|
||||||
|
|
||||||
|
## Decision Criteria
|
||||||
|
|
||||||
|
- **APPROVE**: No security issues found, safe to install
|
||||||
|
- **APPROVE_WITH_WARNINGS**: Minor concerns but generally safe, user should be aware
|
||||||
|
- **REJECT**: Critical security issues found, do not install
|
||||||
|
|
||||||
|
Be thorough but avoid false positives. Consider the context and legitimate use cases.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Analysis
|
||||||
|
|
||||||
|
### Safe Skill Example
|
||||||
|
|
||||||
|
```
|
||||||
|
**Security Status:** SAFE
|
||||||
|
**Risk Level:** LOW
|
||||||
|
**Findings:** None
|
||||||
|
**Summary:** The skill contains only documentation and safe tool usage instructions. No executable code or suspicious patterns detected.
|
||||||
|
**Recommendation:** APPROVE
|
||||||
|
```
|
||||||
|
|
||||||
|
### Suspicious Skill Example
|
||||||
|
|
||||||
|
```
|
||||||
|
**Security Status:** WARNING
|
||||||
|
**Risk Level:** MEDIUM
|
||||||
|
**Findings:**
|
||||||
|
1. [Network Access]: External HTTP request detected
|
||||||
|
- File: scripts/helper.py:42
|
||||||
|
- Severity: MEDIUM
|
||||||
|
- Details: Script makes HTTP request to api.example.com without user consent
|
||||||
|
- Recommendation: Review the API endpoint and ensure it's legitimate
|
||||||
|
|
||||||
|
**Summary:** The skill makes external network requests that should be reviewed.
|
||||||
|
**Recommendation:** APPROVE_WITH_WARNINGS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dangerous Skill Example
|
||||||
|
|
||||||
|
```
|
||||||
|
**Security Status:** DANGEROUS
|
||||||
|
**Risk Level:** CRITICAL
|
||||||
|
**Findings:**
|
||||||
|
1. [Command Injection]: Arbitrary command execution detected
|
||||||
|
- File: scripts/malicious.py:15
|
||||||
|
- Severity: CRITICAL
|
||||||
|
- Details: Uses subprocess.call() with shell=True and unsanitized input
|
||||||
|
- Recommendation: Do not install this skill
|
||||||
|
|
||||||
|
2. [Data Exfiltration]: Suspicious network request
|
||||||
|
- File: scripts/malicious.py:28
|
||||||
|
- Severity: HIGH
|
||||||
|
- Details: Sends data to pastebin.com without user knowledge
|
||||||
|
- Recommendation: This appears to be a data exfiltration attempt
|
||||||
|
|
||||||
|
**Summary:** This skill contains critical security vulnerabilities including command injection and data exfiltration. It appears to be malicious.
|
||||||
|
**Recommendation:** REJECT
|
||||||
|
```
|
||||||
302
uninstall.py
Executable file
302
uninstall.py
Executable file
@@ -0,0 +1,302 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Uninstaller for myclaude - reads installed_modules.json for precise removal."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional, Set
|
||||||
|
|
||||||
|
DEFAULT_INSTALL_DIR = "~/.claude"
|
||||||
|
|
||||||
|
# Files created by installer itself (not by modules)
|
||||||
|
INSTALLER_FILES = ["install.log", "installed_modules.json", "installed_modules.json.bak"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Uninstall myclaude")
|
||||||
|
parser.add_argument(
|
||||||
|
"--install-dir",
|
||||||
|
default=DEFAULT_INSTALL_DIR,
|
||||||
|
help="Installation directory (defaults to ~/.claude)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--module",
|
||||||
|
help="Comma-separated modules to uninstall (default: all installed)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
action="store_true",
|
||||||
|
help="List installed modules and exit",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Show what would be removed without actually removing",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--purge",
|
||||||
|
action="store_true",
|
||||||
|
help="Remove entire install directory (DANGEROUS: removes user files too)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-y", "--yes",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip confirmation prompt",
|
||||||
|
)
|
||||||
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def load_installed_modules(install_dir: Path) -> Dict[str, Any]:
|
||||||
|
"""Load installed_modules.json to know what was installed."""
|
||||||
|
status_file = install_dir / "installed_modules.json"
|
||||||
|
if not status_file.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with status_file.open("r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(install_dir: Path) -> Dict[str, Any]:
|
||||||
|
"""Try to load config.json from source repo to understand module structure."""
|
||||||
|
# Look for config.json in common locations
|
||||||
|
candidates = [
|
||||||
|
Path(__file__).parent / "config.json",
|
||||||
|
install_dir / "config.json",
|
||||||
|
]
|
||||||
|
for path in candidates:
|
||||||
|
if path.exists():
|
||||||
|
try:
|
||||||
|
with path.open("r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
continue
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_module_files(module_name: str, config: Dict[str, Any]) -> Set[str]:
|
||||||
|
"""Extract files/dirs that a module installs based on config.json operations."""
|
||||||
|
files: Set[str] = set()
|
||||||
|
modules = config.get("modules", {})
|
||||||
|
module_cfg = modules.get(module_name, {})
|
||||||
|
|
||||||
|
for op in module_cfg.get("operations", []):
|
||||||
|
op_type = op.get("type", "")
|
||||||
|
target = op.get("target", "")
|
||||||
|
|
||||||
|
if op_type == "copy_file" and target:
|
||||||
|
files.add(target)
|
||||||
|
elif op_type == "copy_dir" and target:
|
||||||
|
files.add(target)
|
||||||
|
elif op_type == "merge_dir":
|
||||||
|
# merge_dir merges subdirs like commands/, agents/ into install_dir
|
||||||
|
source = op.get("source", "")
|
||||||
|
source_path = Path(__file__).parent / source
|
||||||
|
if source_path.exists():
|
||||||
|
for subdir in source_path.iterdir():
|
||||||
|
if subdir.is_dir():
|
||||||
|
files.add(subdir.name)
|
||||||
|
elif op_type == "run_command":
|
||||||
|
# install.sh installs bin/codeagent-wrapper
|
||||||
|
cmd = op.get("command", "")
|
||||||
|
if "install.sh" in cmd or "install.bat" in cmd:
|
||||||
|
files.add("bin/codeagent-wrapper")
|
||||||
|
files.add("bin")
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_shell_config(rc_file: Path, bin_dir: Path) -> bool:
|
||||||
|
"""Remove PATH export added by installer from shell config."""
|
||||||
|
if not rc_file.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
content = rc_file.read_text(encoding="utf-8")
|
||||||
|
original = content
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
r"\n?# Added by myclaude installer\n",
|
||||||
|
rf'\nexport PATH="{re.escape(str(bin_dir))}:\$PATH"\n?',
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
content = re.sub(pattern, "\n", content)
|
||||||
|
|
||||||
|
content = re.sub(r"\n{3,}$", "\n\n", content)
|
||||||
|
|
||||||
|
if content != original:
|
||||||
|
rc_file.write_text(content, encoding="utf-8")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def list_installed(install_dir: Path) -> None:
|
||||||
|
"""List installed modules."""
|
||||||
|
status = load_installed_modules(install_dir)
|
||||||
|
modules = status.get("modules", {})
|
||||||
|
|
||||||
|
if not modules:
|
||||||
|
print("No modules installed (installed_modules.json not found or empty)")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Installed modules in {install_dir}:")
|
||||||
|
print(f"{'Module':<15} {'Status':<10} {'Installed At'}")
|
||||||
|
print("-" * 50)
|
||||||
|
for name, info in modules.items():
|
||||||
|
st = info.get("status", "unknown")
|
||||||
|
ts = info.get("installed_at", "unknown")[:19]
|
||||||
|
print(f"{name:<15} {st:<10} {ts}")
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: Optional[List[str]] = None) -> int:
|
||||||
|
args = parse_args(argv)
|
||||||
|
install_dir = Path(args.install_dir).expanduser().resolve()
|
||||||
|
bin_dir = install_dir / "bin"
|
||||||
|
|
||||||
|
if not install_dir.exists():
|
||||||
|
print(f"Install directory not found: {install_dir}")
|
||||||
|
print("Nothing to uninstall.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.list:
|
||||||
|
list_installed(install_dir)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Load installation status
|
||||||
|
status = load_installed_modules(install_dir)
|
||||||
|
installed_modules = status.get("modules", {})
|
||||||
|
config = load_config(install_dir)
|
||||||
|
|
||||||
|
# Determine which modules to uninstall
|
||||||
|
if args.module:
|
||||||
|
selected = [m.strip() for m in args.module.split(",") if m.strip()]
|
||||||
|
# Validate
|
||||||
|
for m in selected:
|
||||||
|
if m not in installed_modules:
|
||||||
|
print(f"Error: Module '{m}' is not installed")
|
||||||
|
print("Use --list to see installed modules")
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
selected = list(installed_modules.keys())
|
||||||
|
|
||||||
|
if not selected and not args.purge:
|
||||||
|
print("No modules to uninstall.")
|
||||||
|
print("Use --list to see installed modules, or --purge to remove everything.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Collect files to remove
|
||||||
|
files_to_remove: Set[str] = set()
|
||||||
|
for module_name in selected:
|
||||||
|
files_to_remove.update(get_module_files(module_name, config))
|
||||||
|
|
||||||
|
# Add installer files if removing all modules
|
||||||
|
if set(selected) == set(installed_modules.keys()):
|
||||||
|
files_to_remove.update(INSTALLER_FILES)
|
||||||
|
|
||||||
|
# Show what will be removed
|
||||||
|
print(f"Install directory: {install_dir}")
|
||||||
|
if args.purge:
|
||||||
|
print(f"\n⚠️ PURGE MODE: Will remove ENTIRE directory including user files!")
|
||||||
|
else:
|
||||||
|
print(f"\nModules to uninstall: {', '.join(selected)}")
|
||||||
|
print(f"\nFiles/directories to remove:")
|
||||||
|
for f in sorted(files_to_remove):
|
||||||
|
path = install_dir / f
|
||||||
|
exists = "✓" if path.exists() else "✗ (not found)"
|
||||||
|
print(f" {f} {exists}")
|
||||||
|
|
||||||
|
# Confirmation
|
||||||
|
if not args.yes and not args.dry_run:
|
||||||
|
prompt = "\nProceed with uninstallation? [y/N] "
|
||||||
|
response = input(prompt).strip().lower()
|
||||||
|
if response not in ("y", "yes"):
|
||||||
|
print("Aborted.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("\n[Dry run] No files were removed.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"\nUninstalling...")
|
||||||
|
removed: List[str] = []
|
||||||
|
|
||||||
|
if args.purge:
|
||||||
|
shutil.rmtree(install_dir)
|
||||||
|
print(f" ✓ Removed {install_dir}")
|
||||||
|
removed.append(str(install_dir))
|
||||||
|
else:
|
||||||
|
# Remove files/dirs in reverse order (files before parent dirs)
|
||||||
|
for item in sorted(files_to_remove, key=lambda x: x.count("/"), reverse=True):
|
||||||
|
path = install_dir / item
|
||||||
|
if not path.exists():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
if path.is_dir():
|
||||||
|
# Only remove if empty or if it's a known module dir
|
||||||
|
if item in ("bin",):
|
||||||
|
# For bin, only remove codeagent-wrapper
|
||||||
|
wrapper = path / "codeagent-wrapper"
|
||||||
|
if wrapper.exists():
|
||||||
|
wrapper.unlink()
|
||||||
|
print(f" ✓ Removed bin/codeagent-wrapper")
|
||||||
|
removed.append("bin/codeagent-wrapper")
|
||||||
|
# Remove bin if empty
|
||||||
|
if path.exists() and not any(path.iterdir()):
|
||||||
|
path.rmdir()
|
||||||
|
print(f" ✓ Removed empty bin/")
|
||||||
|
else:
|
||||||
|
shutil.rmtree(path)
|
||||||
|
print(f" ✓ Removed {item}/")
|
||||||
|
removed.append(item)
|
||||||
|
else:
|
||||||
|
path.unlink()
|
||||||
|
print(f" ✓ Removed {item}")
|
||||||
|
removed.append(item)
|
||||||
|
except OSError as e:
|
||||||
|
print(f" ✗ Failed to remove {item}: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
# Update installed_modules.json
|
||||||
|
status_file = install_dir / "installed_modules.json"
|
||||||
|
if status_file.exists() and selected != list(installed_modules.keys()):
|
||||||
|
# Partial uninstall: update status file
|
||||||
|
for m in selected:
|
||||||
|
installed_modules.pop(m, None)
|
||||||
|
if installed_modules:
|
||||||
|
with status_file.open("w", encoding="utf-8") as f:
|
||||||
|
json.dump({"modules": installed_modules}, f, indent=2)
|
||||||
|
print(f" ✓ Updated installed_modules.json")
|
||||||
|
|
||||||
|
# Remove install dir if empty
|
||||||
|
if install_dir.exists() and not any(install_dir.iterdir()):
|
||||||
|
install_dir.rmdir()
|
||||||
|
print(f" ✓ Removed empty install directory")
|
||||||
|
|
||||||
|
# Clean shell configs
|
||||||
|
for rc_name in (".bashrc", ".zshrc"):
|
||||||
|
rc_file = Path.home() / rc_name
|
||||||
|
if cleanup_shell_config(rc_file, bin_dir):
|
||||||
|
print(f" ✓ Cleaned PATH from {rc_name}")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
if removed:
|
||||||
|
print(f"✓ Uninstallation complete ({len(removed)} items removed)")
|
||||||
|
else:
|
||||||
|
print("✓ Nothing to remove")
|
||||||
|
|
||||||
|
if install_dir.exists() and any(install_dir.iterdir()):
|
||||||
|
remaining = list(install_dir.iterdir())
|
||||||
|
print(f"\nNote: {len(remaining)} items remain in {install_dir}")
|
||||||
|
print("These are either user files or from other modules.")
|
||||||
|
print("Use --purge to remove everything (DANGEROUS).")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
225
uninstall.sh
Executable file
225
uninstall.sh
Executable file
@@ -0,0 +1,225 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
INSTALL_DIR="${INSTALL_DIR:-$HOME/.claude}"
|
||||||
|
BIN_DIR="${INSTALL_DIR}/bin"
|
||||||
|
STATUS_FILE="${INSTALL_DIR}/installed_modules.json"
|
||||||
|
DRY_RUN=false
|
||||||
|
PURGE=false
|
||||||
|
YES=false
|
||||||
|
LIST_ONLY=false
|
||||||
|
MODULES=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $0 [OPTIONS]
|
||||||
|
|
||||||
|
Uninstall myclaude modules.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--install-dir DIR Installation directory (default: ~/.claude)
|
||||||
|
--module MODULES Comma-separated modules to uninstall (default: all)
|
||||||
|
--list List installed modules and exit
|
||||||
|
--dry-run Show what would be removed without removing
|
||||||
|
--purge Remove entire install directory (DANGEROUS)
|
||||||
|
-y, --yes Skip confirmation prompt
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$0 --list # List installed modules
|
||||||
|
$0 --dry-run # Preview what would be removed
|
||||||
|
$0 --module dev # Uninstall only 'dev' module
|
||||||
|
$0 -y # Uninstall all without confirmation
|
||||||
|
$0 --purge -y # Remove everything (DANGEROUS)
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--install-dir) INSTALL_DIR="$2"; BIN_DIR="${INSTALL_DIR}/bin"; STATUS_FILE="${INSTALL_DIR}/installed_modules.json"; shift 2 ;;
|
||||||
|
--module) MODULES="$2"; shift 2 ;;
|
||||||
|
--list) LIST_ONLY=true; shift ;;
|
||||||
|
--dry-run) DRY_RUN=true; shift ;;
|
||||||
|
--purge) PURGE=true; shift ;;
|
||||||
|
-y|--yes) YES=true; shift ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if install dir exists
|
||||||
|
if [ ! -d "$INSTALL_DIR" ]; then
|
||||||
|
echo "Install directory not found: $INSTALL_DIR"
|
||||||
|
echo "Nothing to uninstall."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List installed modules
|
||||||
|
list_modules() {
|
||||||
|
if [ ! -f "$STATUS_FILE" ]; then
|
||||||
|
echo "No modules installed (installed_modules.json not found)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
echo "Installed modules in $INSTALL_DIR:"
|
||||||
|
echo "Module Status Installed At"
|
||||||
|
echo "--------------------------------------------------"
|
||||||
|
# Parse JSON with basic tools (no jq dependency)
|
||||||
|
python3 -c "
|
||||||
|
import json, sys
|
||||||
|
try:
|
||||||
|
with open('$STATUS_FILE') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
for name, info in data.get('modules', {}).items():
|
||||||
|
status = info.get('status', 'unknown')
|
||||||
|
ts = info.get('installed_at', 'unknown')[:19]
|
||||||
|
print(f'{name:<15} {status:<10} {ts}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error reading status file: {e}', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$LIST_ONLY" = true ]; then
|
||||||
|
list_modules
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get installed modules from status file
|
||||||
|
get_installed_modules() {
|
||||||
|
if [ ! -f "$STATUS_FILE" ]; then
|
||||||
|
echo ""
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
with open('$STATUS_FILE') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
print(' '.join(data.get('modules', {}).keys()))
|
||||||
|
except:
|
||||||
|
print('')
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTALLED=$(get_installed_modules)
|
||||||
|
|
||||||
|
# Determine modules to uninstall
|
||||||
|
if [ -n "$MODULES" ]; then
|
||||||
|
SELECTED="$MODULES"
|
||||||
|
else
|
||||||
|
SELECTED="$INSTALLED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$SELECTED" ] && [ "$PURGE" != true ]; then
|
||||||
|
echo "No modules to uninstall."
|
||||||
|
echo "Use --list to see installed modules, or --purge to remove everything."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Install directory: $INSTALL_DIR"
|
||||||
|
|
||||||
|
if [ "$PURGE" = true ]; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ PURGE MODE: Will remove ENTIRE directory including user files!"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "Modules to uninstall: $SELECTED"
|
||||||
|
echo ""
|
||||||
|
echo "Files/directories that may be removed:"
|
||||||
|
for item in commands agents skills docs bin CLAUDE.md install.log installed_modules.json; do
|
||||||
|
if [ -e "${INSTALL_DIR}/${item}" ]; then
|
||||||
|
echo " $item ✓"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Confirmation
|
||||||
|
if [ "$YES" != true ] && [ "$DRY_RUN" != true ]; then
|
||||||
|
echo ""
|
||||||
|
read -p "Proceed with uninstallation? [y/N] " response
|
||||||
|
case "$response" in
|
||||||
|
[yY]|[yY][eE][sS]) ;;
|
||||||
|
*) echo "Aborted."; exit 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = true ]; then
|
||||||
|
echo ""
|
||||||
|
echo "[Dry run] No files were removed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Uninstalling..."
|
||||||
|
|
||||||
|
if [ "$PURGE" = true ]; then
|
||||||
|
rm -rf "$INSTALL_DIR"
|
||||||
|
echo " ✓ Removed $INSTALL_DIR"
|
||||||
|
else
|
||||||
|
# Remove codeagent-wrapper binary
|
||||||
|
if [ -f "${BIN_DIR}/codeagent-wrapper" ]; then
|
||||||
|
rm -f "${BIN_DIR}/codeagent-wrapper"
|
||||||
|
echo " ✓ Removed bin/codeagent-wrapper"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove bin directory if empty
|
||||||
|
if [ -d "$BIN_DIR" ] && [ -z "$(ls -A "$BIN_DIR" 2>/dev/null)" ]; then
|
||||||
|
rmdir "$BIN_DIR"
|
||||||
|
echo " ✓ Removed empty bin/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove installed directories
|
||||||
|
for dir in commands agents skills docs; do
|
||||||
|
if [ -d "${INSTALL_DIR}/${dir}" ]; then
|
||||||
|
rm -rf "${INSTALL_DIR}/${dir}"
|
||||||
|
echo " ✓ Removed ${dir}/"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Remove installed files
|
||||||
|
for file in CLAUDE.md install.log installed_modules.json installed_modules.json.bak; do
|
||||||
|
if [ -f "${INSTALL_DIR}/${file}" ]; then
|
||||||
|
rm -f "${INSTALL_DIR}/${file}"
|
||||||
|
echo " ✓ Removed ${file}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Remove install directory if empty
|
||||||
|
if [ -d "$INSTALL_DIR" ] && [ -z "$(ls -A "$INSTALL_DIR" 2>/dev/null)" ]; then
|
||||||
|
rmdir "$INSTALL_DIR"
|
||||||
|
echo " ✓ Removed empty install directory"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up PATH from shell config files
|
||||||
|
cleanup_shell_config() {
|
||||||
|
local rc_file="$1"
|
||||||
|
if [ -f "$rc_file" ]; then
|
||||||
|
if grep -q "# Added by myclaude installer" "$rc_file" 2>/dev/null; then
|
||||||
|
# Create backup
|
||||||
|
cp "$rc_file" "${rc_file}.bak"
|
||||||
|
# Remove myclaude lines
|
||||||
|
grep -v "# Added by myclaude installer" "$rc_file" | \
|
||||||
|
grep -v "export PATH=\"${BIN_DIR}:\$PATH\"" > "${rc_file}.tmp"
|
||||||
|
mv "${rc_file}.tmp" "$rc_file"
|
||||||
|
echo " ✓ Cleaned PATH from $(basename "$rc_file")"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_shell_config "$HOME/.bashrc"
|
||||||
|
cleanup_shell_config "$HOME/.zshrc"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✓ Uninstallation complete"
|
||||||
|
|
||||||
|
# Check for remaining files
|
||||||
|
if [ -d "$INSTALL_DIR" ] && [ -n "$(ls -A "$INSTALL_DIR" 2>/dev/null)" ]; then
|
||||||
|
remaining=$(ls -1 "$INSTALL_DIR" 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
echo ""
|
||||||
|
echo "Note: $remaining items remain in $INSTALL_DIR"
|
||||||
|
echo "These are either user files or from other modules."
|
||||||
|
echo "Use --purge to remove everything (DANGEROUS)."
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user