How Agent Reach's Configuration System Manages YAML Storage and Environment Variable Overrides

Agent Reach stores user-level settings in a private YAML file at ~/.agent-reach/config.yaml, with environment variables providing automatic overrides when YAML keys are absent.

The Config class in agent_reach/config.py implements a secure, layered configuration system that prioritizes file-based persistence while allowing flexible runtime overrides. This design lets developers commit non-sensitive defaults to version control while injecting secrets through environment variables in CI/CD or containerized deployments.

YAML File Structure and Location

Agent Reach uses a dedicated hidden directory in the user's home folder for all configuration storage.


# From agent_reach/config.py (lines 1-3)

CONFIG_DIR = Path.home() / ".agent-reach"
CONFIG_FILE = CONFIG_DIR / "config.yaml"

The directory and file are created on first use with strict permissions, ensuring no other users on the system can read or write configuration data.

Secure File Operations

Before any read or write operation, the configuration system validates that no component of the path is a symbolic link. This prevents attackers from redirecting credential reads to malicious files.


# From agent_reach/config.py (lines 38-43)

def _reject_symlink(self, path: Path) -> None:
    """Raise ConfigSecurityError if *path* or any parent is a symlink."""
    ensure_no_symlink_path(path)  # Defined in agent_reach/utils/paths.py

The helper ensure_no_symlink_path traverses the entire path hierarchy, checking Path.is_symlink() at each level.

Atomic YAML Writes

Configuration updates use an atomic write pattern to prevent data corruption if the process crashes mid-operation.


# Simplified from agent_reach/config.py (lines 45-79)

def _atomic_write_yaml(self, data: dict) -> None:
    tmp_path = self.CONFIG_FILE.with_suffix(".tmp")
    with open(tmp_path, "w") as f:
        yaml.safe_dump(data, f)
        f.flush()
        os.fsync(f.fileno())  # Ensure data reaches physical storage

    
    os.replace(tmp_path, self.CONFIG_FILE)  # Atomic rename on POSIX/Windows

    
    # Restrict to owner-only access (0o600)

    self.CONFIG_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)

This pattern guarantees that the file is never in a partially-written state. The fsync call ensures durability even if the operating system crashes immediately after the operation.

Loading and Caching Mechanism

The load() method reads configuration with size limits and symlink protection.


# From agent_reach/config.py (lines 31-50)

def load(self) -> None:
    self._reject_symlink(self.CONFIG_FILE)
    
    if not self.CONFIG_FILE.exists():
        self.data = {}
        return
    
    content = read_small_text_no_follow(self.CONFIG_FILE, max_bytes=1_048_576)
    self.data = yaml.safe_load(content) or {}

The read_small_text_no_follow utility (from agent_reach/utils/paths.py) enforces a 1 MiB maximum, preventing memory exhaustion from malformed or malicious files.

Environment Variable Override System

The get() method implements a two-tier lookup: YAML values take precedence, with environment variables as fallback.


# From agent_reach/config.py (lines 58-66)

def get(self, key: str, default: Any = None) -> Any:
    # First: check the in-memory YAML data

    if key in self.data:
        return self.data[key]
    
    # Second: check uppercase environment variable

    env_val = os.environ.get(key.upper())
    if env_val is not None:
        return env_val
    
    return default

This design choice—YAML over environment—means that explicit file-based configuration always wins, while environment variables provide convenient defaults. The uppercase transformation (key.upper()) creates a predictable mapping: a config key openai_api_key checks OPENAI_API_KEY.

Practical Override Example

from agent_reach.config import Config
import os

cfg = Config()

# Set in YAML via previous save()

cfg.set("api_timeout", 30)

# Environment variable is ignored while YAML value exists

os.environ["API_TIMEOUT"] = "60"
print(cfg.get("api_timeout"))  # → 30

# Delete from YAML to activate environment fallback

cfg.delete("api_timeout")
print(cfg.get("api_timeout"))  # → "60"

Read-Only Mode for Safe Access

Configuration instances can be locked to prevent accidental modifications.


# From agent_reach/config.py (lines 51-58)

def __init__(self, read_only: bool = False):
    self.read_only = read_only
    self.load()

def save(self) -> None:
    if self.read_only:
        raise ConfigReadOnlyError("Cannot save in read-only mode")
    # ... atomic write logic

This is useful when passing configuration to untrusted code or background workers that should not persist changes.

Feature Gating with Configuration Validation

Agent Reach uses a declarative system to check whether required configuration exists for optional features.


# From agent_reach/config.py (lines 5-12)

FEATURE_REQUIREMENTS = {
    "github": ["github_token"],
    "openai": ["openai_api_key", "openai_organization"],
    "anthropic": ["anthropic_api_key"],
}

The is_configured() method leverages the same get() logic, respecting both YAML and environment sources.


# From agent_reach/config.py (lines 99-102)

def is_configured(self, feature: str) -> bool:
    required = FEATURE_REQUIREMENTS.get(feature, [])
    return all(self.get(key) is not None for key in required)

Masked Export for Safe Logging

The to_dict() method redacts sensitive values to prevent credential leaks.


# From agent_reach/config.py (lines 110-132)

def to_dict(self) -> dict:
    result = {}
    for key, value in self.data.items():
        if any(s in key.lower() for s in ("key", "token", "secret", "password")):
            result[key] = value[:4] + "..." if isinstance(value, str) else "***"
        else:
            result[key] = value
    return result

A value like ghp_xxxxxxxxxxxxxxxxxxxx becomes ghp..., sufficient for debugging without exposing credentials.

Complete Usage Example

from agent_reach.config import Config
import os

# Initialize with default path ~/.agent-reach/config.yaml

cfg = Config()

# Persist a setting (atomic write with 0o600 permissions)

cfg.set("github_token", "ghp_xxxxxxxxxxxxxxxxxxxx")

# Override via environment for CI pipeline

os.environ["ANTHROPIC_API_KEY"] = "sk-ant-xxx"

# Retrieve with automatic fallback chain

github_token = cfg.get("github_token")      # From YAML

anthropic_key = cfg.get("anthropic_api_key")  # From env (not in YAML)

# Check feature readiness

if cfg.is_configured("github"):
    print("GitHub integration ready")

# Safe export for logs

print(cfg.to_dict())  # {"github_token": "ghp...", ...}

Summary

  • File location: ~/.agent-reach/config.yaml with owner-only permissions (0o600)
  • Security: Symlink rejection via ensure_no_symlink_path in agent_reach/utils/paths.py
  • Atomicity: _atomic_write_yaml uses temp file + fsync + rename pattern
  • Override precedence: YAML values override environment variables; keys are matched case-insensitively via uppercase transformation
  • Safety features: Read-only mode, masked export, and 1 MiB read limits prevent common configuration vulnerabilities

Frequently Asked Questions

Can I change the configuration file location?

No, the path is hardcoded to ~/.agent-reach/config.yaml via Path.home() constants. This design prevents path confusion attacks and ensures consistent behavior across the codebase. For multi-environment deployments, use environment variable overrides rather than multiple config files.

Why does YAML take precedence over environment variables?

This prioritization allows users to temporarily lock a value by persisting it, even when an environment variable exists. It also prevents surprise behavior when environment variables are set system-wide but a user has explicitly configured a different value.

How does Agent Reach prevent credential leaks in crash reports?

The to_dict() method automatically masks any key containing key, token, secret, or password substrings. Additionally, __repr__ is not overridden to expose raw data, and the Config class does not implement direct dict access—developers must explicitly call get() or to_dict().

What happens if the config file is corrupted?

yaml.safe_load raises a yaml.error.YAMLError on parse failure, which propagates uncaught in load(). The calling code should wrap configuration initialization in appropriate exception handling. Empty or missing files default to an empty dictionary without error.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →