How Agent Reach Persists Configuration in ~/.agent-reach/config.yaml

Agent Reach stores user-specific settings in a YAML file at ~/.agent-reach/config.yaml using a secure Config class that handles directory creation, atomic writes with 600 permissions, and environment variable fallback.

Agent Reach is an open-source Python framework hosted at Panniantong/Agent-Reach that manages user-specific settings through a local configuration file. The persistence logic resides in agent_reach/config.py and ensures sensitive data like API keys are stored securely with restricted file permissions. This implementation is tested in tests/test_config.py and relies on PyYAML (declared in pyproject.toml) for serialization.

Configuration File Location and Structure

Default Path Resolution

The configuration system resolves the storage path using Python's pathlib module. According to the source code in agent_reach/config.py, the framework defines:

  • CONFIG_DIR as Path.home() / ".agent-reach" (line 18)
  • CONFIG_FILE as CONFIG_DIR / "config.yaml" (line 19)

This creates a hidden directory in the user's home folder, following Unix conventions for application-specific data.

The Config Class Persistence Mechanism

Lazy Directory Creation with _ensure_dir()

Before writing any data, the private method _ensure_dir() ensures the configuration directory exists. Found at lines 37-40 in agent_reach/config.py, this method uses Path.mkdir(parents=True, exist_ok=True) to create missing parent directories without raising errors if the path already exists. This method is invoked during class initialization and prior to every save operation.

Loading Existing Configuration

The load() method (lines 41-48) handles configuration retrieval by checking for file existence first. If config.yaml exists, it parses the content using yaml.safe_load; otherwise, it initializes an empty dictionary. This approach prevents FileNotFoundError exceptions on first run while safely handling YAML parsing.

Secure File Writing with Atomic Permissions

The save() method implements platform-aware security measures to prevent credential leakage.

On Unix systems (lines 49-60), it uses low-level os.open() with flags O_WRONLY | O_CREAT | O_TRUNC and mode 0o600 (owner read/write only). This atomic creation prevents race conditions where temporary files might be world-readable.

For Windows compatibility (lines 63-68), the method falls back to standard open() when os.open flags aren't supported, ensuring cross-platform functionality.

The configuration data is serialized using yaml.dump() with default_flow_style=False and allow_unicode=True for human-readable output.

Accessor Methods and Environment Integration

Reading Configuration Values

The get(key, default) method (lines 69-78) implements a two-tier lookup strategy:

  1. Check the in-memory configuration dictionary
  2. Fall back to uppercase environment variables if the key is absent

This allows environment variables to override file-based settings without modifying the YAML.

Writing and Deleting Keys

The set(key, value) method (lines 80-84) updates the internal dictionary and immediately calls save() to persist changes to disk. Similarly, delete(key) (lines 85-88) removes the specified key and triggers a save operation, ensuring the file system remains synchronized with the in-memory state.

Feature-Specific Configuration Validation

Checking Feature Requirements

The FEATURE_REQUIREMENTS dictionary maps optional features (like "exa_search") to their required configuration keys (e.g., ["exa_api_key"]). The is_configured(feature) method (lines 90-94) returns True only when all required keys exist in the configuration or environment variables.

Aggregating Feature Status

get_configured_features() (lines 95-100) iterates through all defined features and returns those with complete configurations, enabling the application to dynamically enable functionality based on available credentials.

Security and Privacy Protections

Credential Masking

The to_dict() method (lines 102-108) provides a sanitized view of the configuration by masking values for keys containing sensitive substrings: "key", "token", "password", or "proxy". This prevents accidental credential exposure in logs or debug output.

Working with Agent Reach Configuration

from agent_reach.config import Config

# Initialize (loads existing config or creates empty one)

cfg = Config()

# Set API key - automatically persisted with 600 permissions

cfg.set('exa_api_key', 'sk-abcdef123456')

# Retrieve value (checks file first, then environment)

api_key = cfg.get('exa_api_key')

# Remove sensitive data

cfg.delete('exa_api_key')

# Verify feature readiness

if cfg.is_configured('exa_search'):
    print("EXA search is ready")

Summary

  • Agent Reach persists configuration in ~/.agent-reach/config.yaml using the Config class from agent_reach/config.py
  • The _ensure_dir() method creates the directory structure on demand with exist_ok=True
  • File writes use os.open() with 0o600 permissions on Unix to ensure only the owner can read sensitive data
  • The get() method supports environment variable fallback for containerized deployments
  • Feature validation via is_configured() ensures required API keys are present before enabling functionality
  • to_dict() masks sensitive values to prevent credential leakage in logs

Frequently Asked Questions

How does Agent Reach handle first-time setup when the config file doesn't exist?

When initializing the Config class for the first time, the load() method checks for file existence. If ~/.agent-reach/config.yaml is missing, it initializes an empty dictionary rather than raising an error. The directory and file are created only when set() or save() is first called, utilizing the _ensure_dir() method to create parent directories automatically.

Why does Agent Reach use file mode 0o600 when saving configuration?

The save() method uses os.open() with mode 0o600 (owner read/write only) to prevent other users on the system from accessing sensitive credentials like API keys. This creates the file with restrictive permissions atomically at creation time, avoiding race conditions where temporary files might be readable by others. On Windows, where these Unix permissions aren't available, it gracefully falls back to standard file operations.

Can environment variables override the YAML configuration file?

Yes. The get() method implements a priority system where it first checks the in-memory configuration dictionary loaded from the YAML file. If the key is not found, it automatically checks for an uppercase environment variable with the same name. This allows Docker containers and CI/CD pipelines to inject credentials without modifying the local config file.

How does Agent Reach prevent API keys from appearing in logs?

The to_dict() method sanitizes the configuration before returning it by masking any value where the key contains sensitive substrings like "key", "token", "password", or "proxy". When debugging or logging the configuration object, developers should use to_dict() rather than accessing the raw internal dictionary to avoid accidentally exposing credentials in log files or error reports.

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 →