Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/paude/agents/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class AgentConfig:
secret_env_vars: Host env vars to deliver securely (not in container spec).
passthrough_env_prefixes: Host env var prefixes to forward.
config_dir_name: Config directory under HOME (e.g., ".claude").
config_dir_env_var: Env var that overrides the host-side config directory
path (e.g., "CLAUDE_CONFIG_DIR"). When set, the host config is read
from this path instead of HOME/config_dir_name. Does not affect
container-side paths.
config_file_name: Config file under HOME (e.g., ".claude.json"), or None.
config_excludes: Rsync excludes for config sync.
config_sync_files_only: When non-empty, only these files (relative to
Expand Down Expand Up @@ -52,6 +56,7 @@ class AgentConfig:
secret_env_vars: list[str] = field(default_factory=list)
passthrough_env_prefixes: list[str] = field(default_factory=list)
config_dir_name: str = ".claude"
config_dir_env_var: str | None = None
config_file_name: str | None = ".claude.json"
config_excludes: list[str] = field(default_factory=list)
config_sync_files_only: list[str] = field(default_factory=list)
Expand Down
8 changes: 6 additions & 2 deletions src/paude/agents/claude.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import os
from pathlib import Path

from paude.agents.base import (
Expand Down Expand Up @@ -59,6 +60,7 @@ def __init__(self, provider: str | None = None) -> None:
secret_env_vars=creds.secret_env_vars,
passthrough_env_prefixes=creds.passthrough_env_prefixes,
config_dir_name=".claude",
config_dir_env_var="CLAUDE_CONFIG_DIR",
config_file_name=".claude.json",
config_excludes=list(_CLAUDE_CONFIG_EXCLUDES),
activity_files=list(_CLAUDE_ACTIVITY_FILES),
Expand Down Expand Up @@ -138,8 +140,10 @@ def launch_command(self, args: str) -> str:
def host_config_mounts(self, home: Path) -> list[str]:
mounts: list[str] = []

# Claude seed directory (ro)
claude_dir = home / ".claude"
# Claude seed directory (ro) — honour CLAUDE_CONFIG_DIR override
env_var = self._config.config_dir_env_var
custom_config_dir = os.environ.get(env_var) if env_var else None
claude_dir = Path(custom_config_dir) if custom_config_dir else home / ".claude"
resolved_claude = resolve_path(claude_dir)
if resolved_claude and resolved_claude.is_dir():
mounts.extend(["-v", f"{resolved_claude}:/tmp/claude.seed:ro"])
Expand Down
12 changes: 10 additions & 2 deletions src/paude/backends/sync_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import annotations

import os
from abc import ABC, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -78,15 +79,22 @@ def _sync_config_files(self, agent_name: str) -> None:
self._sync_gitconfig(home)
self._sync_global_gitignore(home)
if config_synced:
plugins_dir = home / agent.config.config_dir_name / "plugins"
host_config = self._host_config_dir(agent, home)
plugins_dir = host_config / "plugins"
if plugins_dir.is_dir():
self._rewrite_plugin_paths(agent_path, agent, home)

# -- shared step implementations ---------------------------------------

def _host_config_dir(self, agent: Agent, home: Path) -> Path:
"""Resolve the host-side config directory, respecting env overrides."""
env_var = agent.config.config_dir_env_var
custom = os.environ.get(env_var) if env_var else None
return Path(custom) if custom else home / agent.config.config_dir_name

def _sync_agent_config(self, agent_path: str, agent: Agent, home: Path) -> bool:
"""Sync agent config directory. Returns True on success."""
config_dir = home / agent.config.config_dir_name
config_dir = self._host_config_dir(agent, home)
if not config_dir.is_dir():
return True

Expand Down
26 changes: 26 additions & 0 deletions tests/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,32 @@ def test_mounts_claude_json(self, tmp_path: Path) -> None:
mounts = ClaudeAgent().host_config_mounts(tmp_path)
assert any("/tmp/claude.json.seed:ro" in m for m in mounts)

def test_respects_claude_config_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
custom_dir = tmp_path / "my-claude-config"
custom_dir.mkdir()
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(custom_dir))
mounts = ClaudeAgent().host_config_mounts(tmp_path)
assert any(str(custom_dir) in m and "/tmp/claude.seed:ro" in m for m in mounts)

def test_claude_config_dir_with_plugins(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
custom_dir = tmp_path / "my-claude-config"
custom_dir.mkdir()
plugins = custom_dir / "plugins"
plugins.mkdir()
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(custom_dir))
mounts = ClaudeAgent().host_config_mounts(tmp_path)
assert any("plugins" in m and ":ro" in m for m in mounts)

def test_claude_config_dir_ignores_default(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
# Default .claude dir exists but CLAUDE_CONFIG_DIR points elsewhere
(tmp_path / ".claude").mkdir()
custom_dir = tmp_path / "other"
custom_dir.mkdir()
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(custom_dir))
mounts = ClaudeAgent().host_config_mounts(tmp_path)
assert not any(".claude" in m and "/tmp/claude.seed:ro" in m for m in mounts)
assert any(str(custom_dir) in m and "/tmp/claude.seed:ro" in m for m in mounts)


class TestClaudeAgentBuildEnvironment:
"""Tests for ClaudeAgent.build_environment."""
Expand Down