How the Plan Approval Hash Is Generated in Claude-Obsidian: SHA-256 Binding Explained

The plan approval hash is generated by the plan_approval_sha256 function in claude_obsidian/transaction.py, which creates a deterministic SHA-256 digest from canonical JSON containing the vault's filesystem identity, the expanded operation bundle, and prepared write metadata.

Claude-obsidian implements a cryptographic safety mechanism to ensure that approved file operations can only be applied to the exact vault and contents they were reviewed against. This mechanism centers on the plan approval hash, a SHA-256 digest computed through a multi-step canonicalization process that binds the operation bundle to specific filesystem state and content hashes.

The Core Function: plan_approval_sha256

The hash generation logic resides in claude_obsidian/transaction.py at lines 1287–1308 within the plan_approval_sha256 function. This function orchestrates the collection of identity metadata, bundle contents, and write projections before serializing them into a format suitable for cryptographic hashing.

The function accepts four primary parameters:

  • vault_root – The absolute path or user-defined label for the target vault
  • vault_identity – A JSON object describing the vault's filesystem identity (device and inode)
  • expanded_bundle – The reviewed operation bundle containing all planned changes
  • prepared_writes – A projection of pending file writes with original and new SHA-256 values

Step-by-Step Hash Generation Process

The generation follows three distinct phases to ensure deterministic, collision-resistant output.

Step 1: Collecting Canonical Identity and Bundle Data

First, the function assembles a structured data object containing four critical fields:

  • vault_root – Captures the absolute pathname or user-provided vault label
  • vault_identity – A JSON object containing device, inode, and state flags describing the vault's filesystem location; if not provided, _vault_object_identity generates this automatically
  • expanded_bundle_sha256 – The SHA-256 hash of the expanded operation bundle, computed via _canonical_json_hash (which delegates to bundle_sha256)
  • prepared_writes – A projection of every pending write generated by _prepared_projection, including relative paths, intended modes, original SHA-256 hashes, and new content hashes

These fields are assembled into a single JSON object with the fixed schema identifier "claude-obsidian.plan-approval.v3", ensuring version compatibility.

Step 2: Canonical JSON Serialization

The bundle_sha256 helper function (lines 1011–1022 in transaction.py) handles serialization with strict parameters to guarantee byte-for-byte consistency:

json.dumps(
    data,
    sort_keys=True,           # Deterministic key ordering

    separators=(",", ":"),      # Minimal whitespace (no spaces after delimiters)

    ensure_ascii=False,         # Unicode support without escaping

    allow_nan=False            # Strict JSON compliance

)

This canonicalization eliminates formatting variations that could produce different hashes across Python versions or platforms.

Step 3: SHA-256 Calculation via sha256_bytes

Finally, the serialized UTF-8 byte string passes to sha256_bytes, imported from claude_obsidian/json_utils.py. This utility computes the cryptographic hash and returns the hexadecimal digest string that serves as the plan approval hash.

Cryptographic Binding and Replay Protection

Because the hash incorporates the vault's device and inode identifiers alongside exact file content hashes, any change to the vault location, file contents, or intended permissions produces a completely different digest. This prevents accidental or malicious replay of old approvals against modified vaults or different filesystem locations.

The vault_identity field specifically protects against vault path renaming or migration attacks, while the prepared_writes array ensures that the exact byte-level changes reviewed are the only ones that can be applied.

Implementation Example: Generating and Validating the Hash

from pathlib import Path
from claude_obsidian.transaction import (
    plan_approval_sha256,
    inspect_bundle,
)

# Reviewed operation bundle and prepared writes

expanded_bundle = {"ops": [...]}  # The reviewed plan

prepared_writes = [
    {
        "relative_path": "wiki/page.md",
        "mode": 0o100644,
        "original_sha256": "a1b2c3...",
        "original_mode": 0o100644,
        "content_sha256": "d4e5f6...",
        "new_mode": 0o100644,
    },
    # Additional writes...

]

# Generate approval hash

vault_path = Path("/path/to/vault")
approval_hash = plan_approval_sha256(
    vault_root=vault_path,
    expanded_bundle=expanded_bundle,
    prepared_writes=prepared_writes,
)

print(f"Plan approval SHA-256: {approval_hash}")

# Later validation during application

bundle_info = inspect_bundle(vault_path, operation)
assert bundle_info["approval_sha256"] == approval_hash, "Approval mismatch detected"

Key Source Files and Their Roles

Summary

  • The plan approval hash is generated by plan_approval_sha256 in claude_obsidian/transaction.py using a three-step canonicalization process
  • The hash binds to vault filesystem identity (device/inode), preventing approval reuse across different vault locations
  • Canonical JSON serialization with sort_keys=True and minimal separators ensures deterministic, cross-platform hash consistency
  • The hash includes SHA-256 values of both original and new file contents, guaranteeing that only reviewed changes can be applied
  • Validation occurs through inspect_bundle, which compares stored hashes against recomputed values during plan execution

Frequently Asked Questions

What specific data fields are included in the plan approval hash?

The hash includes four canonical fields: the vault_root path, a vault_identity JSON object containing device and inode information, the expanded_bundle_sha256 of the operation plan, and the prepared_writes array containing path, mode, and content hashes for every affected file. These are serialized under the schema identifier "claude-obsidian.plan-approval.v3".

How does claude-obsidian ensure the hash is deterministic across different systems?

The bundle_sha256 function enforces deterministic output by using json.dumps() with sort_keys=True to ensure consistent key ordering, separators=(",", ":") to eliminate whitespace variance, and ensure_ascii=False with allow_nan=False for strict, predictable JSON encoding. This canonicalization guarantees that identical data produces identical byte sequences for hashing regardless of the host platform.

Where is the plan approval hash validated during plan execution?

Validation occurs when inspect_bundle retrieves the stored approval hash from the operation metadata and compares it against a freshly computed hash of the current vault state and bundle contents. This check ensures the vault identity matches and the file contents haven't changed since the original review, effectively preventing the application of stale or tampered plans.

What happens if file contents or vault paths change after the hash is generated?

Any modification to the vault path (which changes device/inode), file contents (altering SHA-256 values), or intended file modes will cause the plan_approval_sha256 function to produce a different digest. When inspect_bundle validates the approval, this mismatch triggers an assertion failure or error, halting the operation and protecting against unintended modifications or replay attacks.

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 →