# Validation Steps for Source Capture in Claude-Obsidian: The 9-Stage Security Pipeline

> Explore the 9-stage security pipeline for source capture validation in claude-obsidian. Learn how it ensures configuration integrity, path containment, filename safety, and more before vault entry.

- Repository: [Agrici.Daniel/claude-obsidian](https://github.com/AgriciDaniel/claude-obsidian)
- Tags: how-to-guide
- Published: 2026-08-29

---

**Claude-obsidian enforces a strict nine-stage validation pipeline that verifies configuration integrity, path containment, filename safety, content identity, resource budgets, and URL security before any file or URL enters the vault.**

The `claude-obsidian` tool by AgriciDaniel implements a deterministic input policy designed to guarantee that every captured source is safe, reproducible, and compliant with resource limits. Before any write operation occurs, the system validates inputs through a rigorous sequence of checks defined primarily in [`claude_obsidian/capture.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/capture.py). Only sources passing all stages proceed to the write phase via `plan_filesystem_batch` or `capture_filesystem_batch`.

## Configuration and Directory Structure Validation

The pipeline begins by establishing a secure foundation through configuration verification and path normalization.

### Stage 1: Load Capture Configuration

The system first reads optional JSON configuration and validates critical paths. The **`CaptureConfig.load`** method (delegating to **`__post_init__`**) verifies that the `inbox` and `raw_store` directories exist and are visible (not hidden symlinks). This ensures the vault structure is intact before any source evaluation begins.

### Stage 2: Validate Relative Directory Strings

Before path resolution, **`_validate_relative_directory`** (line 90) enforces POSIX-relative formatting. Directory strings must contain no backslashes, must not escape the vault hierarchy via parent traversal (`..`), and must conform to portable path standards. This prevents injection attacks through malformed directory parameters.

## Path Resolution and Containment

Local filesystem sources undergo rigorous containment checks to prevent directory traversal and symlink attacks.

### Stage 3: Resolve Source Path

The **`_allowed_source_path`** function (lines 85‑106) converts source strings to absolute, real paths using physical filesystem lookups. It explicitly rejects missing files, non-regular file types (directories, devices), and refuses to follow symlinks during resolution. This eliminates indirection attacks at the entry point.

### Stage 4: Ensure Source Is Inside Allowed Root

Within the same **`_allowed_source_path`** routine (lines 101‑124), the system verifies that the resolved absolute path resides under one of the configured source roots (`inbox` or the legacy `.raw` directory). It performs an iterative component check to ensure no intermediate directory in the path is a symlink, blocking symlink-based escape attempts that could traverse outside the vault.

## Content Integrity Verification

Once paths are secured, the system validates the actual content identity and naming conventions.

### Stage 5: Validate Filename Portability

The **`_validate_filename`** function enforces cross-platform safety rules: rejecting reserved Windows names (like `CON`, `PRN`), ensuring Unicode NFC normalization, capping length at 240 bytes, and disallowing control characters, trailing spaces, or trailing dots. Violations raise `CaptureValidationError` with the code `UNSAFE_FILENAME`.

### Stage 6: Compute Immutable Source Identity

To guarantee reproducibility, **`source_identity`** (lines 38‑53) computes a SHA‑256 hash of the file content. The function opens files with `O_NOFOLLOW` flags to prevent TOCTOU (time-of-check to time-of-use) race conditions through symlinks. This creates a cryptographic fingerprint that detects any modification occurring between validation and capture.

## Resource Enforcement

The pipeline applies hard caps to prevent resource exhaustion attacks.

### Stage 7: Enforce Capture Budget Limits

The **`_preflight_sources`** function (lines 94‑118) applies three strict quotas: `max_items` (total file count), `max_total_bytes` (aggregate size), and `max_file_bytes` (individual file size). Sources exceeding any threshold trigger `CaptureBudgetExceeded`, a subclass of `CaptureValidationError`, preventing vault bloat and denial-of-service scenarios.

## Network Source Validation

URLs undergo specialized checks distinct from filesystem validation.

### Stage 8: Validate HTTPS URL

**`validate_https_url`** (lines 80‑124) inspects external resources for protocol safety. It mandates HTTPS scheme, validates host visibility ( rejecting private IP ranges like `192.168.x.x` or `10.x.x.x`), enforces port ranges, blocks fragments, and checks for credential leakage via `url_credential_issue` (from [`url_safety.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/url_safety.py)). Malformed URLs or those pointing to internal networks raise errors with codes like `URL_PRIVATE_HOST`.

### Stage 9: Validate Redirect Chain

After fetch resolution, **`validate_redirect_chain`** (lines 32‑48) examines every intermediate hop in a redirect sequence. Each URL in the chain must pass the same HTTPS security rules, and the final host must appear in the adapter's approved host allow-list. This prevents open-redirect vulnerabilities and ensures external adapters communicate only with authorized endpoints.

## Error Handling and Violation Codes

Any validation failure raises a specific subclass of **`CaptureValidationError`**, providing machine-readable error codes for programmatic handling:

- **`SOURCE_OUTSIDE_INBOX`**: The resolved path escapes the configured source roots.
- **`UNSAFE_FILENAME`**: Filename contains reserved characters, excessive length, or reserved Windows base names.
- **`URL_PRIVATE_HOST`**: URL resolves to a private or loopback address.
- **`CaptureBudgetExceeded`**: File size or count exceeds configured limits.

These exceptions halt the capture process before `plan_filesystem_batch` returns, ensuring atomicity—either all sources validate or the entire operation aborts.

## Implementation Examples

### Validating Local Files for Batch Capture

Use `plan_filesystem_batch` to execute the complete validation pipeline against local sources:

```python
from pathlib import Path
from claude_obsidian.capture import plan_filesystem_batch

vault = Path("/path/to/vault")
sources = [Path("inbox/report.md"), Path("inbox/images/photo.png")]

# Runs stages 1-7: configuration, path resolution, filename checks,

# identity hashing, and budget enforcement.

plan = plan_filesystem_batch(vault, sources)

for entry in plan:
    print(entry["source"], "→", entry["stored_path"], 
          "(will change:", entry["would_change"], ")")

```

If any source violates validation rules—such as residing outside the inbox or exceeding the byte budget—`CaptureValidationError` raises immediately with a specific error code before the function returns.

### Validating HTTPS URLs Before External Adapter Execution

For network sources, explicitly validate URLs before planning external actions:

```python
from claude_obsidian.capture import validate_https_url, plan_external_action

url = "https://example.com/data.csv"

# Raises CaptureValidationError for malformed URLs or private hosts.

canonical = validate_https_url(url)

action = plan_external_action(
    adapter_id="url",
    source=canonical,
    approved_hosts=["example.com"],  # Optional explicit allow-list

    runner=["/usr/local/bin/http-downloader"]
)

print("Planned command:", action["command"])

```

The `plan_external_action` routine internally invokes `validate_redirect_chain` to ensure the entire redirect path complies with security policies.

## Core Source Files

The validation pipeline spans four critical modules:

- **[`claude_obsidian/capture.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/capture.py)**: Central implementation containing all nine validation stages, budget enforcement, and planning functions (`plan_filesystem_batch`, `validate_https_url`).
- **[`claude_obsidian/paths.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/paths.py)**: Provides `canonical` and `is_relative_to` utilities used to prevent symlink-traversal attacks during path resolution.
- **[`claude_obsidian/url_safety.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/url_safety.py)**: Detects credential leakage in URLs through `url_credential_issue`, consulted during HTTPS validation.
- **[`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py)**: Defines `CaptureValidationError` subclasses and supplies the transactional API (`apply_bundle`) used after validation succeeds.

## Summary

- **Nine-stage pipeline**: Configuration loading, directory validation, path resolution, root containment, filename sanitization, content hashing, budget enforcement, URL validation, and redirect verification.
- **Security guarantees**: No symlink traversal, no path escapes, no private network access, and deterministic content identity via SHA‑256.
- **Hard limits**: Enforced quotas prevent resource exhaustion through `max_items`, `max_total_bytes`, and `max_file_bytes`.
- **Atomic validation**: `CaptureValidationError` subclasses provide specific error codes (`SOURCE_OUTSIDE_INBOX`, `UNSAFE_FILENAME`, `URL_PRIVATE_HOST`) before any filesystem modification occurs.

## Frequently Asked Questions

### What happens if a source file is located outside the inbox directory?

The **`_allowed_source_path`** function raises `CaptureValidationError` with code `SOURCE_OUTSIDE_INBOX`. The system resolves the absolute path and verifies it resides under configured roots (`inbox` or `.raw`), rejecting any file attempting to traverse outside these boundaries via parent directory references or symlinks.

### How does claude-obsidian prevent symlink attacks during capture?

Multiple defenses exist: **`_allowed_source_path`** uses `O_NOFOLLOW` when opening files and iterates through path components to detect symlinks. The `source_identity` function also opens files with no-follow flags to prevent race conditions. Additionally, [`claude_obsidian/paths.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/paths.py) provides canonicalization utilities that resolve physical paths without following symbolic links, blocking traversal attacks at stages 3 and 4.

### What filename restrictions does the validation pipeline enforce?

**`_validate_filename`** rejects Windows reserved device names (CON, PRN, AUX, NUL, COM1‑9, LPT1‑9), requires Unicode NFC normalization, limits length to 240 bytes, and blocks control characters, trailing spaces, and trailing dots. These rules ensure cross-platform compatibility and prevent filesystem-specific exploitation vectors.

### How are HTTP redirects handled during URL validation?

**`validate_redirect_chain`** (lines 32‑48) inspects every hop in a redirect sequence. Each intermediate URL must pass the same HTTPS validation as the original, including scheme checks and host visibility verification. The final destination must also match the adapter's approved host allow-list, preventing attackers from using redirects to reach unauthorized or private internal services.