From 4f1689c13c5e7295c8e1be66b1ee0735b07882b3 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 11:45:42 +0000 Subject: [PATCH 1/5] feat: default seguro para working_dir en claude-code provider Cuando WorkingDir esta vacio, se crea un directorio temporal aislado en lugar de heredar el CWD del launcher (raiz del repo). Esto evita que el subproceso claude -p tenga acceso de lectura/escritura al codigo fuente del proyecto. Si WorkingDir tiene valor, se asegura que el directorio exista creandolo con MkdirAll. Se loguea WARN cuando se usa el tmpdir para que el operador lo note y configure explicitamente. Co-Authored-By: Claude Opus 4.6 --- shell/llm/claudecode.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/shell/llm/claudecode.go b/shell/llm/claudecode.go index f410f5c..95c7e87 100644 --- a/shell/llm/claudecode.go +++ b/shell/llm/claudecode.go @@ -58,6 +58,26 @@ func NewClaudeCodeComplete(cfg config.ClaudeCodeCfg, log *slog.Logger) coretypes timeout = defaultClaudeTimeout } + // Resolve working directory once at init time. + workDir := cfg.WorkingDir + if workDir == "" { + tmp, err := os.MkdirTemp("", "claude-agent-*") + if err != nil { + log.Error("claude-code: failed to create temp working dir", "err", err) + // Fall through — cmd.Dir will remain empty (inherits CWD). + } else { + workDir = tmp + log.Warn("claude-code working_dir is empty, using temporary directory", + "dir", workDir, + ) + } + } else { + // Ensure configured directory exists. + if err := os.MkdirAll(workDir, 0o755); err != nil { + log.Error("claude-code: failed to create working dir", "dir", workDir, "err", err) + } + } + return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() @@ -70,11 +90,12 @@ func NewClaudeCodeComplete(cfg config.ClaudeCodeCfg, log *slog.Logger) coretypes "binary", binary, "args", strings.Join(args, " "), "prompt_len", len(prompt), + "working_dir", workDir, ) cmd := exec.CommandContext(ctx, binary, args...) - if cfg.WorkingDir != "" { - cmd.Dir = cfg.WorkingDir + if workDir != "" { + cmd.Dir = workDir } // Build clean env: inherit parent but remove ANTHROPIC_API_KEY // so claude uses its own OAuth auth instead of a potentially invalid key. From d05ec0bd574e41f67166819e5e03b2d9b2ec07ba Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 11:46:01 +0000 Subject: [PATCH 2/5] feat: configurar working_dir aislado para ambos agentes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setear working_dir a /tmp/claude-agents/ en assistant-bot y asistente-2, evitando que claude -p herede el CWD del launcher (raiz del repo). El directorio se crea automaticamente al arrancar gracias al MkdirAll añadido en el commit anterior. Se mantiene bypassPermissions ya que el aislamiento real viene del working_dir — sin acceso al repo, el bypass no expone codigo fuente. Co-Authored-By: Claude Opus 4.6 --- agents/asistente-2/config.yaml | 2 +- agents/assistant-bot/config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agents/asistente-2/config.yaml b/agents/asistente-2/config.yaml index ff2a4dd..98affbe 100644 --- a/agents/asistente-2/config.yaml +++ b/agents/asistente-2/config.yaml @@ -54,7 +54,7 @@ llm: disable_tools: true # no ejecuta herramientas internas de claude allowed_tools: [] disallowed_tools: [] - working_dir: "" + working_dir: "/tmp/claude-agents/asistente-2" permission_mode: "bypassPermissions" model: "sonnet" fallback_model: "" diff --git a/agents/assistant-bot/config.yaml b/agents/assistant-bot/config.yaml index e555f12..f0c6436 100644 --- a/agents/assistant-bot/config.yaml +++ b/agents/assistant-bot/config.yaml @@ -54,7 +54,7 @@ llm: disable_tools: true # no ejecuta herramientas internas de claude allowed_tools: [] disallowed_tools: [] - working_dir: "" + working_dir: "/tmp/claude-agents/assistant-bot" permission_mode: "bypassPermissions" model: "sonnet" # modelo interno de claude -p fallback_model: "" From 6a5cad5700b0d0b6c16e05e6654f194183e129d8 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 11:47:07 +0000 Subject: [PATCH 3/5] test: extraer resolveWorkDir y tests unitarios de aislamiento MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extraer la logica de resolucion de working_dir a una funcion resolveWorkDir() separada para hacerla testeable. Tres tests cubren: - WorkingDir vacio → crea tmpdir con prefijo claude-agent-* - WorkingDir configurado → crea el directorio y lo usa - WorkingDir ya existente → lo usa sin error Co-Authored-By: Claude Opus 4.6 --- shell/llm/claudecode.go | 42 ++++++++++++++++------------ shell/llm/claudecode_test.go | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/shell/llm/claudecode.go b/shell/llm/claudecode.go index 95c7e87..50d8217 100644 --- a/shell/llm/claudecode.go +++ b/shell/llm/claudecode.go @@ -59,24 +59,7 @@ func NewClaudeCodeComplete(cfg config.ClaudeCodeCfg, log *slog.Logger) coretypes } // Resolve working directory once at init time. - workDir := cfg.WorkingDir - if workDir == "" { - tmp, err := os.MkdirTemp("", "claude-agent-*") - if err != nil { - log.Error("claude-code: failed to create temp working dir", "err", err) - // Fall through — cmd.Dir will remain empty (inherits CWD). - } else { - workDir = tmp - log.Warn("claude-code working_dir is empty, using temporary directory", - "dir", workDir, - ) - } - } else { - // Ensure configured directory exists. - if err := os.MkdirAll(workDir, 0o755); err != nil { - log.Error("claude-code: failed to create working dir", "dir", workDir, "err", err) - } - } + workDir := resolveWorkDir(cfg.WorkingDir, log) return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) { ctx, cancel := context.WithTimeout(ctx, timeout) @@ -141,6 +124,29 @@ func NewClaudeCodeComplete(cfg config.ClaudeCodeCfg, log *slog.Logger) coretypes } } +// resolveWorkDir determines the working directory for the claude subprocess. +// If configured is empty, it creates a temporary directory to avoid inheriting the launcher's CWD. +// If configured is non-empty, it ensures the directory exists. +func resolveWorkDir(configured string, log *slog.Logger) string { + if configured == "" { + tmp, err := os.MkdirTemp("", "claude-agent-*") + if err != nil { + log.Error("claude-code: failed to create temp working dir", "err", err) + return "" // Fall through — cmd.Dir will remain empty (inherits CWD). + } + log.Warn("claude-code working_dir is empty, using temporary directory", + "dir", tmp, + ) + return tmp + } + + // Ensure configured directory exists. + if err := os.MkdirAll(configured, 0o755); err != nil { + log.Error("claude-code: failed to create working dir", "dir", configured, "err", err) + } + return configured +} + // buildClaudeArgs constructs the CLI arguments for claude -p. func buildClaudeArgs(cfg config.ClaudeCodeCfg, req coretypes.CompletionRequest) []string { args := []string{"--print", "--output-format", "json"} diff --git a/shell/llm/claudecode_test.go b/shell/llm/claudecode_test.go index a8d414d..07c87d0 100644 --- a/shell/llm/claudecode_test.go +++ b/shell/llm/claudecode_test.go @@ -5,6 +5,9 @@ import ( "errors" "io" "log/slog" + "os" + "path/filepath" + "strings" "testing" "time" @@ -318,6 +321,56 @@ func TestFilterEnv_PrefixSafety(t *testing.T) { } } +// ── resolveWorkDir ────────────────────────────────────────────────────── + +func TestResolveWorkDir_EmptyCreatesTempDir(t *testing.T) { + dir := resolveWorkDir("", discardLog) + if dir == "" { + t.Fatal("expected a temp directory, got empty string") + } + defer os.RemoveAll(dir) + + if !strings.Contains(dir, "claude-agent-") { + t.Errorf("temp dir %q should contain 'claude-agent-' prefix", dir) + } + + info, err := os.Stat(dir) + if err != nil { + t.Fatalf("temp dir should exist: %v", err) + } + if !info.IsDir() { + t.Error("temp dir should be a directory") + } +} + +func TestResolveWorkDir_ConfiguredValueUsed(t *testing.T) { + want := filepath.Join(t.TempDir(), "custom-workdir") + + got := resolveWorkDir(want, discardLog) + + if got != want { + t.Errorf("got %q, want %q", got, want) + } + + info, err := os.Stat(got) + if err != nil { + t.Fatalf("configured dir should be created: %v", err) + } + if !info.IsDir() { + t.Error("configured dir should be a directory") + } +} + +func TestResolveWorkDir_ConfiguredAlreadyExists(t *testing.T) { + want := t.TempDir() // already exists + + got := resolveWorkDir(want, discardLog) + + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + // ── helpers ────────────────────────────────────────────────────────────── func contains(s, substr string) bool { From de34d8a99deb3666e81adee9c4f78b01dd9d7216 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 11:48:13 +0000 Subject: [PATCH 4/5] docs: documentar aislamiento de claude -p en security y guias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/security.md: nueva seccion 6 sobre aislamiento del provider claude-code, comportamiento del working_dir y checklist actualizado - CLAUDE.md: añadir claude_code.working_dir a la seccion de seguridad - create_agent.md: recomendar siempre configurar working_dir cuando se usa el provider claude-code Co-Authored-By: Claude Opus 4.6 --- .claude/CLAUDE.md | 3 ++- .claude/rules/create_agent.md | 12 ++++++++++++ docs/security.md | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 271db94..028934d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -100,8 +100,9 @@ Protecciones contra prompt injection y abuso de tools (issue 0019): - **Rate limiting** — por room en `tools/registry.go` via `security.tool_rate_limit` - **System prompts** — seccion anti-injection obligatoria (template en `.claude/templates/security-prompt.md`) - **`storage.base_path`** — permite aislar datos de runtime fuera del arbol del proyecto +- **`claude_code.working_dir`** — aislamiento del subproceso `claude -p` fuera del repo (default: tmpdir) -Config YAML relevante: `security.sanitize.*`, `security.tool_rate_limit.*`, `storage.base_path` +Config YAML relevante: `security.sanitize.*`, `security.tool_rate_limit.*`, `storage.base_path`, `claude_code.working_dir` Documentacion completa: `docs/security.md` ## Preferencias diff --git a/.claude/rules/create_agent.md b/.claude/rules/create_agent.md index 4578467..bdd2e70 100644 --- a/.claude/rules/create_agent.md +++ b/.claude/rules/create_agent.md @@ -84,6 +84,18 @@ llm: api_key_env: ANTHROPIC_API_KEY # o OPENAI_API_KEY (default) ``` +**Claude-code provider** (si usa `claude-code` como provider): +```yaml +llm: + primary: + provider: claude-code + claude_code: + working_dir: "/tmp/claude-agents/" # SIEMPRE configurar, nunca dejar vacío + permission_mode: "bypassPermissions" +``` + +**Importante**: `working_dir` debe apuntar fuera del repositorio para evitar que el subproceso `claude -p` acceda al código fuente. Si se deja vacío, se usará un directorio temporal (con WARN en logs). + **Tool use** (si el agente necesita herramientas): ```yaml llm: diff --git a/docs/security.md b/docs/security.md index 13b2873..ae3d26f 100644 --- a/docs/security.md +++ b/docs/security.md @@ -115,6 +115,28 @@ Prioridad: config `base_path` > `$AGENTS_DATA_DIR/` > `agents//data/` (d Esto previene que tools como `read_file` accedan accidentalmente a codigo fuente, `.env`, o configs del proyecto. +## 6. Aislamiento de claude -p (provider claude-code) + +Cuando un agente usa el provider `claude-code`, el subproceso `claude -p` se ejecuta en un directorio de trabajo aislado, no en la raiz del repositorio. + +**Configuracion:** + +```yaml +llm: + primary: + claude_code: + working_dir: "/tmp/claude-agents/mi-bot" # directorio aislado +``` + +**Comportamiento:** +- Si `working_dir` esta configurado: se crea el directorio automaticamente con `MkdirAll` y se usa como CWD del subproceso +- Si `working_dir` esta vacio: se crea un directorio temporal (`os.MkdirTemp`) y se loguea un WARN para que el operador lo note +- **Nunca** se hereda el CWD del launcher (raiz del repo) + +Esto evita que el subproceso `claude -p` tenga acceso de lectura/escritura al codigo fuente del proyecto, incluso con `permission_mode: bypassPermissions`. + +Implementado en `shell/llm/claudecode.go` → `resolveWorkDir()`. + ## Activacion Para activar todas las protecciones, añadir al `config.yaml` del agente: @@ -133,3 +155,4 @@ Y asegurarse de que: - Las tools tienen allowlists configuradas (no vacias si se quieren usar) - El system prompt incluye la seccion de seguridad - `storage.base_path` apunta fuera del proyecto en produccion +- `claude_code.working_dir` apunta fuera del repo si se usa el provider claude-code From 9045d5a21450b9c42a8633581edfac522c027720 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 11:48:34 +0000 Subject: [PATCH 5/5] chore: cerrar issue 0020 y mover a completed Todas las tareas del issue implementadas: default seguro con tmpdir, configuracion de agentes existentes, tests unitarios y documentacion. Co-Authored-By: Claude Opus 4.6 --- dev/issues/README.md | 2 +- dev/issues/{ => completed}/0020-claude-code-sandbox.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename dev/issues/{ => completed}/0020-claude-code-sandbox.md (100%) diff --git a/dev/issues/README.md b/dev/issues/README.md index 95efce4..63f0e9c 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -24,4 +24,4 @@ afectados y notas de implementacion. | 17 | MCP client tools | [0017-mcp-client-tools.md](0017-mcp-client-tools.md) | pendiente | | 18 | Shared knowledge | [0018-shared-knowledge.md](0018-shared-knowledge.md) | pendiente | | 19 | Prompt injection hardening | [0019-prompt-injection-hardening.md](completed/0019-prompt-injection-hardening.md) | completado | -| 20 | Aislar claude -p del repo | [0020-claude-code-sandbox.md](0020-claude-code-sandbox.md) | pendiente | +| 20 | Aislar claude -p del repo | [0020-claude-code-sandbox.md](completed/0020-claude-code-sandbox.md) | completado | diff --git a/dev/issues/0020-claude-code-sandbox.md b/dev/issues/completed/0020-claude-code-sandbox.md similarity index 100% rename from dev/issues/0020-claude-code-sandbox.md rename to dev/issues/completed/0020-claude-code-sandbox.md