# How `inspect_bundle` Validates Write Bundles Without Mutation in Claude-Obsidian

> Learn how claude-obsidian's inspect_bundle validates write bundles without mutation. It uses temporary directories and vault snapshots for safe, zero-side-effect verification.

- Repository: [Agrici.Daniel/claude-obsidian](https://github.com/AgriciDaniel/claude-obsidian)
- Tags: internals
- Published: 2026-08-26

---

**`inspect_bundle` guarantees zero side-effects by performing all validation steps—including write preparation and cryptographic hashing—inside isolated temporary directories while capturing and verifying vault identity snapshots before and after processing.**

The `inspect_bundle` function in the `AgriciDaniel/claude-obsidian` repository serves as the read-only entry point that validates transaction bundles before any filesystem mutations occur. This deterministic inspection pipeline computes the complete execution plan—including metadata expansion and content hashing—without creating or modifying files in the target vault. By operating on temporary copies and verifying vault identity through inode and device tracking, the function ensures safe dry-run validation for CI checks and pre-flight verification.

## Vault Path Resolution and Security Validation

The inspection begins with strict path sanitization and security guards that prevent traversal attacks.

**Canonical path verification** occurs first in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) (lines 4365–4372), where the function resolves the supplied `vault_root` to an absolute path and verifies it exists as a directory. If the path is not a directory, the engine raises `[VAULT_NOT_DIRECTORY]`; if the parent is missing for a new vault scenario, it raises `[VAULT_PARENT_MISSING]`.

Immediately following on line 4373, the `_assert_no_portable_vault_root_alias` guard prohibits "portable" aliases that could obscure the vault's real location. This prevents attackers from exploiting symbolic links or mount point confusion to redirect writes to unintended directories.

## Schema and Operation Type Verification

Once the vault path is secured, the function validates the bundle structure and operation semantics.

In lines 4374–4378, `_load_bundle` ingests the input—whether a Python `dict` or a JSON file path—and verifies the top-level schema compliance. The function then sanitizes the `operation_id` and validates the `operation_type` against the allowed set defined in `OPERATION_TYPES` (lines 4379–4384). This ensures the engine only processes recognized transaction types such as `"write"` before proceeding to resource allocation.

## Secure Directory Descriptors and Identity Snapshots

To prevent time-of-check-to-time-of-use (TOCTOU) attacks, the function captures a cryptographic baseline of the vault state.

On platforms supporting confined directory access, lines 4385–4392 open the vault using `os.open(..., directory_open_flags())`. Failure to obtain this file descriptor triggers an `UNSAFE_VAULT_IDENTITY` error. With the descriptor secured, line 4393 calls `_vault_object_identity(vault, root_fd=root_fd)` to capture a collision-resistant snapshot comprising the inode, device ID, and UID. This `vault_identity` fingerprint is stored for later comparison to detect concurrent mutations.

## Metadata Expansion and Isolated Write Preparation

All computationally intensive validation happens in a temporary sandbox, ensuring the real vault remains untouched.

Lines 4396–4397 invoke `_expand_managed_metadata` to compute the concrete bundle state that the executor would see, expanding auto-generated front-matter, timestamps, and aliases through pure computation without disk writes. The expanded bundle then passes to `_prepare_writes` (lines 4399–4407), which creates a `tempfile.TemporaryDirectory` sandbox and resolves every write entry to a `PreparedWrite` object containing the target path, content hash, and file mode. This temporary directory is the *only* filesystem location touched during inspection.

## Integrity Verification and Return Payload

Before returning results, the function verifies the vault remained stable throughout the inspection process.

Lines 4408–4411 perform a vault-identity re-check by comparing the current vault state against the initial snapshot. If the inode or device changed—indicating concurrent mutation—the engine raises `VAULT_IDENTITY_CHANGED`. After confirming integrity, lines 4413–4415 close any opened directory file descriptors to prevent resource leaks.

The function then computes deterministic hashes for reproducibility. Line 4416–4429 calls `_canonical_json_hash` on the expanded bundle and computes a `plan_approval_sha256` from the vault identity, expanded bundle, prepared writes, and vault root. Finally, lines 4429–4440 return a read-only summary dictionary containing:
- `valid: True`
- `changed_paths`: List of affected files
- Per-path content hashes and desired file modes
- `bundle_sha256` and `expanded_bundle_sha256`
- `vault_identity` and `approval_sha256`

No file in the real vault is created or modified during this entire sequence.

## Usage Examples

The following examples demonstrate how to call `inspect_bundle` for safe pre-validation:

```python
from pathlib import Path
from claude_obsidian.transaction import inspect_bundle

# Example 1 – Inspect a bundle file on disk

bundle_path = Path("/tmp/my-transaction-bundle.json")
summary = inspect_bundle(
    vault_root="/home/user/my-vault", 
    bundle_or_path=bundle_path
)
print(summary["valid"])               # → True

print(summary["changed_paths"])       # → ['wiki/Note.md', 'wiki/Folder/Info.md']

```

```python

# Example 2 – Inspect an in-memory bundle dict

bundle = {
    "schema": "claude-obsidian.transaction-bundle.v1",
    "operation_id": "add-note",
    "operation_type": "write",
    "writes": [
        {"path": "wiki/NewNote.md", "mode": "create", "content": "# New Note\n"},

    ],
}
summary = inspect_bundle(
    vault_root="/home/user/my-vault", 
    bundle_or_path=bundle
)
print(summary["approval_sha256"])      # deterministic hash for later approval

```

Both invocations are strictly read-only and safe to execute against production vaults.

## Source File Reference

Understanding the validation pipeline requires familiarity with these key files:

| File | Role |
|------|------|
| [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) | Core implementation of `inspect_bundle`, `apply_bundle`, and all validation helpers referenced in the pipeline steps. |
| [`claude_obsidian/_runtime.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/_runtime.py) | Internal runtime providing `_RuntimeStore` and low-level directory file descriptor handling for secure vault access. |
| [`tests/test_transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/tests/test_transaction.py) | Unit tests verifying `inspect_bundle` validation logic and strict zero-mutation guarantees. |
| [`tests/test_vault_ops.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/tests/test_vault_ops.py) | Integration tests combining inspection with actual bundle application workflows. |

## Summary

- **`inspect_bundle`** validates write bundles through a 12-step deterministic pipeline that never touches the real vault.
- **Security checks** include canonical path resolution, portable-alias guards, and secure directory file descriptors (lines 4365–4392).
- **Zero-mutation guarantee** is achieved by processing all writes inside `tempfile.TemporaryDirectory` sandboxes (lines 4399–4407).
- **Integrity verification** uses pre- and post-processing vault identity snapshots to detect concurrent modifications (lines 4393, 4408–4411).
- **Deterministic output** includes cryptographic hashes for the bundle, expanded metadata, and a plan approval fingerprint suitable for CI/CD gates.

## Frequently Asked Questions

### How does `inspect_bundle` prevent accidental file modification in the vault?

The function routes all write preparation through `_prepare_writes`, which creates a temporary directory via `tempfile.TemporaryDirectory` and resolves all file paths and content hashes within that isolated sandbox. By computing the full execution plan—including metadata expansion and content hashing—without referencing the real vault directory for write operations, the function guarantees zero filesystem side-effects according to the implementation in [`claude_obsidian/transaction.py`](https://github.com/AgriciDaniel/claude-obsidian/blob/main/claude_obsidian/transaction.py) (lines 4396–4407).

### What happens if the vault changes while `inspect_bundle` is running?

The function captures a vault identity snapshot (inode, device, UID) before processing using `_vault_object_identity` (line 4393) and compares it against the current state after preparation completes (lines 4408–4411). If the identity differs—indicating a concurrent modification or external mutation—the engine raises `VAULT_IDENTITY_CHANGED` and aborts, preventing validation against a stale vault state.

### Can `inspect_bundle` handle both file paths and Python dictionaries as input?

Yes. The internal `_load_bundle` helper (lines 4374–4378) accepts either a file path pointing to a JSON bundle or an in-memory Python dictionary. The function normalizes both inputs into a validated bundle structure before proceeding with schema and operation type verification, making it flexible for CLI tools and programmatic API usage.

### What is the `plan_approval_sha256` returned by `inspect_bundle`?

The `plan_approval_sha256` is a deterministic cryptographic hash computed from the vault identity, expanded bundle contents, prepared write objects, and vault root path (lines 4416–4429). This fingerprint allows teams to store a pre-computed approval checksum and verify later that the exact same bundle is being applied to the exact same vault state, enabling secure multi-stage deployment pipelines.