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

> Discover how Agent Reach manages YAML storage and environment variable overrides for seamless configuration. Learn to leverage environment variables to automatically update your settings.

- Repository: [Pnant/Agent-Reach](https://github.com/Panniantong/Agent-Reach)
- Tags: internals
- Published: 2026-08-05

---

**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`](https://github.com/Panniantong/Agent-Reach/blob/main/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.

```python

# 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

### Symlink Attack Prevention

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.

```python

# 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.

```python

# 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.

```python

# 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`](https://github.com/Panniantong/Agent-Reach/blob/main/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.

```python

# 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

```python
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.

```python

# 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.

```python

# 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.

```python

# 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.

```python

# 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

```python
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`](https://github.com/Panniantong/Agent-Reach/blob/main/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.