Skip to content

Configuration Module

Configuration system for managing executor settings and remote targets.

Overview

The config module provides TOML-based configuration management for Shannot executors. It allows you to define reusable executor configurations (local or SSH) with their associated profiles and connection settings.

Key Components:

  • ShannotConfig - Main configuration container with executor targets
  • ExecutorConfig - Base configuration for executors
  • LocalExecutorConfig - Local executor configuration
  • SSHExecutorConfig - SSH executor configuration with connection details
  • load_config() - Load configuration from TOML file

Configuration File Format

Shannot uses TOML for configuration files, typically located at ~/.config/shannot/config.toml:

[executors.production]
type = "ssh"
host = "prod.example.com"
username = "readonly"
key_file = "~/.ssh/prod_key"
profile = "diagnostics"
port = 22

[executors.staging]
type = "ssh"
host = "staging.example.com"
username = "readonly"
key_file = "~/.ssh/staging_key"
profile = "minimal"

[executors.local]
type = "local"
profile = "diagnostics"
bwrap_path = "/usr/bin/bwrap"

Common Usage Patterns

Loading Configuration

from shannot.config import load_config

# Load from default location (~/.config/shannot/config.toml)
config = load_config()

# Load from custom path
config = load_config(Path("/etc/shannot/config.toml"))

Getting Executors

# Get specific executor
executor = config.get_executor("production")

# Use with commands
result = await executor.run_command(profile, ["df", "-h"])

Listing Available Targets

# List all configured executor names
targets = config.list_executors()
print(f"Available targets: {', '.join(targets)}")

CLI Integration

# Use configured target from CLI
shannot --target production df -h

# MCP install on remote target
shannot mcp install claude-code --target staging

Configuration Options

SSH Executor Options

[executors.myserver]
type = "ssh"
host = "server.example.com"          # Required: hostname or IP
username = "readonly"                 # Optional: SSH username (default: current user)
key_file = "~/.ssh/id_rsa"           # Optional: SSH private key path
port = 22                             # Optional: SSH port (default: 22)
profile = "diagnostics"               # Optional: default profile for this target
connection_pool_size = 5              # Optional: max concurrent connections
known_hosts = "~/.ssh/known_hosts"   # Optional: known_hosts file
strict_host_key = true                # Optional: strict host key checking

Local Executor Options

[executors.local]
type = "local"
profile = "minimal"                   # Optional: default profile
bwrap_path = "/usr/bin/bwrap"        # Optional: explicit bwrap path

Environment Variables

Configuration paths can be overridden with environment variables:

# Custom config location
export SHANNOT_CONFIG=~/my-shannot-config.toml
shannot --target prod df -h

# Custom profile
export SANDBOX_PROFILE=~/.config/shannot/diagnostics.json
shannot --target prod df -h

Programmatic Configuration

You can also create configurations programmatically:

from shannot.config import ShannotConfig, SSHExecutorConfig

# Create config
config = ShannotConfig(executors={
    "prod": SSHExecutorConfig(
        type="ssh",
        host="prod.example.com",
        username="readonly",
        key_file=Path("~/.ssh/prod_key"),
        profile="diagnostics"
    )
})

# Use executor
executor = config.get_executor("prod")

API Reference

config

Configuration management for Shannot.

This module handles loading and managing executor configurations from TOML files.

Classes

ExecutorConfig

Bases: BaseModel

Base configuration for an executor.

Source code in shannot/config.py
class ExecutorConfig(BaseModel):
    """Base configuration for an executor."""

    type: ExecutorType
    profile: str | None = None  # Default profile for this executor

LocalExecutorConfig

Bases: ExecutorConfig

Configuration for local executor.

Source code in shannot/config.py
class LocalExecutorConfig(ExecutorConfig):
    """Configuration for local executor."""

    type: Literal["local"] = "local"  # type: ignore[assignment]
    bwrap_path: Path | None = None  # Explicit path to bwrap if needed

SSHExecutorConfig

Bases: ExecutorConfig

Configuration for SSH executor.

Source code in shannot/config.py
class SSHExecutorConfig(ExecutorConfig):
    """Configuration for SSH executor."""

    type: Literal["ssh"] = "ssh"  # type: ignore[assignment]
    host: str
    username: str | None = None
    key_file: Path | None = None
    port: int = 22
    connection_pool_size: int = 5
    known_hosts: Path | None = None
    strict_host_key: bool = True

    @field_validator("key_file", mode="before")
    @classmethod
    def expand_path(cls, v: str | Path | None) -> Path | None:
        """Expand ~ in paths."""
        if v is None:
            return None
        path = Path(v)
        return path.expanduser()

    @field_validator("known_hosts", mode="before")
    @classmethod
    def expand_known_hosts(cls, v: str | Path | None) -> Path | None:
        """Expand ~ in known_hosts paths."""
        if v is None:
            return None
        return Path(v).expanduser()
Functions
expand_path(v) classmethod

Expand ~ in paths.

Source code in shannot/config.py
@field_validator("key_file", mode="before")
@classmethod
def expand_path(cls, v: str | Path | None) -> Path | None:
    """Expand ~ in paths."""
    if v is None:
        return None
    path = Path(v)
    return path.expanduser()
expand_known_hosts(v) classmethod

Expand ~ in known_hosts paths.

Source code in shannot/config.py
@field_validator("known_hosts", mode="before")
@classmethod
def expand_known_hosts(cls, v: str | Path | None) -> Path | None:
    """Expand ~ in known_hosts paths."""
    if v is None:
        return None
    return Path(v).expanduser()

ShannotConfig

Bases: BaseModel

Complete Shannot configuration.

Source code in shannot/config.py
class ShannotConfig(BaseModel):
    """Complete Shannot configuration."""

    default_executor: str = "local"
    executor: dict[str, LocalExecutorConfig | SSHExecutorConfig] = Field(default_factory=dict)

    def get_executor_config(
        self, name: str | None = None
    ) -> LocalExecutorConfig | SSHExecutorConfig:
        """Get executor config by name, or default if name is None."""
        executor_name = name or self.default_executor

        if executor_name not in self.executor:
            available = ", ".join(self.executor.keys())
            raise ValueError(
                f"Executor '{executor_name}' not found in config. Available executors: {available}"
            )

        return self.executor[executor_name]
Functions
get_executor_config(name=None)

Get executor config by name, or default if name is None.

Source code in shannot/config.py
def get_executor_config(
    self, name: str | None = None
) -> LocalExecutorConfig | SSHExecutorConfig:
    """Get executor config by name, or default if name is None."""
    executor_name = name or self.default_executor

    if executor_name not in self.executor:
        available = ", ".join(self.executor.keys())
        raise ValueError(
            f"Executor '{executor_name}' not found in config. Available executors: {available}"
        )

    return self.executor[executor_name]

Functions

get_config_path()

Get the path to the Shannot config file.

Returns: Path to ~/.config/shannot/config.toml (or Windows/macOS equivalent)

Source code in shannot/config.py
def get_config_path() -> Path:
    """Get the path to the Shannot config file.

    Returns:
        Path to ~/.config/shannot/config.toml (or Windows/macOS equivalent)
    """
    if sys.platform == "win32":
        config_dir = Path.home() / "AppData" / "Local" / "shannot"
    elif sys.platform == "darwin":
        config_dir = Path.home() / "Library" / "Application Support" / "shannot"
    else:
        # Linux and other Unix-like systems
        xdg_config = Path(os.environ.get("XDG_CONFIG_HOME", "~/.config")).expanduser()
        config_dir = xdg_config / "shannot"

    return config_dir / "config.toml"

load_config(config_path=None)

Load Shannot configuration from TOML file.

Args: config_path: Optional path to config file. If not provided, uses default.

Returns: Loaded configuration

Raises: FileNotFoundError: If config file doesn't exist ValueError: If config file is invalid

Source code in shannot/config.py
def load_config(config_path: Path | None = None) -> ShannotConfig:
    """Load Shannot configuration from TOML file.

    Args:
        config_path: Optional path to config file. If not provided, uses default.

    Returns:
        Loaded configuration

    Raises:
        FileNotFoundError: If config file doesn't exist
        ValueError: If config file is invalid
    """
    if config_path is None:
        config_path = get_config_path()

    if not config_path.exists():
        # Return default config (local only)
        return ShannotConfig(
            default_executor="local",
            executor={"local": LocalExecutorConfig(type="local")},
        )

    try:
        with open(config_path, "rb") as f:
            data: dict[str, object] = tomllib.load(f)
    except Exception as e:
        raise ValueError(f"Failed to parse config file {config_path}: {e}") from e

    try:
        return ShannotConfig.model_validate(data)
    except Exception as e:
        raise ValueError(f"Invalid config file {config_path}: {e}") from e

save_config(config, config_path=None)

Save Shannot configuration to TOML file.

Args: config: Configuration to save config_path: Optional path to config file. If not provided, uses default.

Source code in shannot/config.py
def save_config(config: ShannotConfig, config_path: Path | None = None) -> None:
    """Save Shannot configuration to TOML file.

    Args:
        config: Configuration to save
        config_path: Optional path to config file. If not provided, uses default.
    """
    if config_path is None:
        config_path = get_config_path()

    # Ensure directory exists
    _ = config_path.parent.mkdir(parents=True, exist_ok=True)

    # Convert to TOML format manually (Pydantic doesn't have TOML export)
    lines = [
        f'default_executor = "{config.default_executor}"',
        "",
    ]

    for name, executor_config in config.executor.items():
        lines.append(f"[executor.{name}]")
        lines.append(f'type = "{executor_config.type}"')

        if executor_config.profile:
            lines.append(f'profile = "{executor_config.profile}"')

        if isinstance(executor_config, SSHExecutorConfig):
            lines.append(f'host = "{executor_config.host}"')
            if executor_config.username:
                lines.append(f'username = "{executor_config.username}"')
            if executor_config.key_file:
                lines.append(f'key_file = "{executor_config.key_file}"')
            if executor_config.port != 22:
                lines.append(f"port = {executor_config.port}")
            if executor_config.connection_pool_size != 5:
                lines.append(f"connection_pool_size = {executor_config.connection_pool_size}")
            if executor_config.known_hosts:
                lines.append(f'known_hosts = "{executor_config.known_hosts}"')
            if not executor_config.strict_host_key:
                lines.append("strict_host_key = false")
        elif isinstance(executor_config, LocalExecutorConfig):
            if executor_config.bwrap_path:
                lines.append(f'bwrap_path = "{executor_config.bwrap_path}"')

        lines.append("")

    with open(config_path, "w") as f:
        f.write("\n".join(lines))

create_executor(config, executor_name=None)

Create an executor from configuration.

Args: config: Shannot configuration executor_name: Name of executor to create, or None for default

Returns: Initialized executor

Raises: ValueError: If executor config is invalid or executor not found

Source code in shannot/config.py
def create_executor(config: ShannotConfig, executor_name: str | None = None) -> SandboxExecutor:
    """Create an executor from configuration.

    Args:
        config: Shannot configuration
        executor_name: Name of executor to create, or None for default

    Returns:
        Initialized executor

    Raises:
        ValueError: If executor config is invalid or executor not found
    """
    executor_config = config.get_executor_config(executor_name)

    if executor_config.type == "local":
        from .executors import LocalExecutor

        return LocalExecutor(bwrap_path=executor_config.bwrap_path)
    elif executor_config.type == "ssh":
        try:
            from .executors import SSHExecutor
        except ImportError as exc:
            message = (
                "SSH executor requires the 'asyncssh' dependency. "
                "Install with: pip install shannot[remote]"
            )
            raise RuntimeError(message) from exc

        return SSHExecutor(
            host=executor_config.host,
            username=executor_config.username,
            key_file=executor_config.key_file,
            port=executor_config.port,
            connection_pool_size=executor_config.connection_pool_size,
            known_hosts=executor_config.known_hosts,
            strict_host_key=executor_config.strict_host_key,
        )
    else:
        raise ValueError(f"Unknown executor type: {executor_config.type}")

get_executor(executor_name=None, config_path=None)

Convenience function to load config and create executor.

Args: executor_name: Name of executor to create, or None for default config_path: Optional path to config file

Returns: Initialized executor

Source code in shannot/config.py
def get_executor(
    executor_name: str | None = None, config_path: Path | None = None
) -> SandboxExecutor:
    """Convenience function to load config and create executor.

    Args:
        executor_name: Name of executor to create, or None for default
        config_path: Optional path to config file

    Returns:
        Initialized executor
    """
    config = load_config(config_path)
    return create_executor(config, executor_name)