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 vaultvault_identity– A JSON object describing the vault's filesystem identity (device and inode)expanded_bundle– The reviewed operation bundle containing all planned changesprepared_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 labelvault_identity– A JSON object containingdevice,inode, andstateflags describing the vault's filesystem location; if not provided,_vault_object_identitygenerates this automaticallyexpanded_bundle_sha256– The SHA-256 hash of the expanded operation bundle, computed via_canonical_json_hash(which delegates tobundle_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
claude_obsidian/transaction.py– Containsplan_approval_sha256,bundle_sha256, and helper functions_vault_object_identityand_prepared_projectionclaude_obsidian/json_utils.py– Providessha256_bytesfor final cryptographic hash computationclaude_obsidian/vault_ops.py– Implements filesystem identity extraction used invault_identitygenerationtests/test_transaction.py– Validates that approval hashes correctly bind to vault identity and detect file mode changes
Summary
- The plan approval hash is generated by
plan_approval_sha256inclaude_obsidian/transaction.pyusing 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=Trueand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →