How claude-obsidian Generates a Canonical Hash for Operation Approval Using `plan_approval_sha256`

The plan_approval_sha256 function in claude_obsidian/transaction.py creates a deterministic SHA‑256 hash by combining the vault’s stable identity, a canonical JSON representation of the operation plan, and a projection of every file write, ensuring only the exact reviewed changes can be executed.

Every write operation in claude-obsidian requires explicit user approval bound to a cryptographic hash. This mechanism prevents accidental or malicious modifications between the review step and the actual file system mutation. The canonical approval hash is produced by a multi-step pipeline that deterministically serializes the vault state, operation intent, and pending changes.

Vault Identity Resolution

Before hashing, claude-obsidian establishes a stable anchor to the target vault. In claude_obsidian/transaction.py, the helper _vault_object_identity (lines 1012–1016) generates a unique identifier derived from the vault directory’s device and inode, or falls back to absent-root metadata if the path does not yet exist. This vault identity ensures the approval hash is valid only for the specific vault instance on the specific machine, preventing the same plan from being accidentally applied to a different vault.

Canonical JSON Serialization of the Operation Plan

The operation plan—referred to as the expanded bundle—is converted to a deterministic byte representation. The function calls _canonical_json_hash, which delegates to bundle_sha256 (lines 1025–1027).

bundle_sha256 performs strict serialization:

  • Sorts all JSON keys alphabetically
  • Uses consistent separators (no trailing whitespace)
  • Encodes the result as UTF‑8 before feeding it to sha256_bytes

This guarantees that logically identical plans always produce identical hashes regardless of key insertion order or formatting variations.

Projection of Prepared File Writes

To capture exact byte-level changes, claude-obsidian projects each pending write into a minimal dictionary structure (lines 1029–1040). Each entry contains:

  • Relative path from the vault root
  • File mode (permissions)
  • Original SHA‑256 hash of the file before modification
  • New content SHA‑256 hash after modification

This projection ensures that any alteration—whether to file contents, permissions, or path—invalidates the approval hash, thwarting replay attacks where a plan is re-executed against different data.

Assembly of the Approval Bundle

plan_approval_sha256 assembles a final JSON object (lines 1087–1095) containing:

{
    "schema": "claude-obsidian.plan-approval.v3",  # fixed identifier

    "vault_root": str(vault_root),                # canonical path or user label

    "vault_identity": vault_identity,             # from step 1

    "expanded_bundle_sha256": bundle_hash,        # from step 2

    "prepared_writes": [writes_projection]        # from step 3

}

The assembled object is hashed once more via bundle_sha256 to produce the canonical approval hash. The fixed schema version (claude-obsidian.plan-approval.v3) enables future backward compatibility checks while ensuring the current format is unambiguous.

Runtime Verification and Security Guarantees

When apply_bundle executes the transaction, it recomputes the approval hash from the current vault state and compares it against the stored approval_sha256 using hmac.compare_digest (lines 4494–4498). This constant-time comparison prevents timing side-channels.

If any component differs—the vault identity (indicating a moved or copied vault), the expanded bundle (indicating logic changes), or the prepared writes (indicating modified file contents or modes)—the comparison fails. The system raises a TransactionValidationError and aborts the operation, enforcing that only the exact reviewed plan can modify the vault.

Practical Implementation Examples

Generating an Approval Hash

from pathlib import Path
from claude_obsidian.transaction import plan_approval_sha256

vault_root = Path("/home/user/obsidian-vault")
expanded_bundle = {"operation": "rename", "target": "daily-notes/2024-01-01.md"}
prepared_writes = []  # List of PreparedWrite objects populated earlier

approval_hash = plan_approval_sha256(
    vault_root=vault_root,
    expanded_bundle=expanded_bundle,
    prepared_writes=prepared_writes,
)
print(f"Canonical approval SHA-256: {approval_hash}")

Applying with Verification

from claude_obsidian.transaction import apply_bundle, inspect_bundle
from pathlib import Path

vault = Path("/home/user/obsidian-vault")
operation_id = "rename-op-42"

# Review phase: compute and store the hash

review_data = inspect_bundle(vault, operation_id)
stored_hash = review_data["approval_sha256"]

# Execution phase: apply only if hashes match

try:
    apply_bundle(vault, operation_id, approved_plan_sha256=stored_hash)
    print("Transaction applied successfully.")
except TransactionValidationError as e:
    print(f"Approval mismatch: {e}")

Summary

  • plan_approval_sha256 in claude_obsidian/transaction.py creates a deterministic SHA‑256 hash that uniquely identifies a reviewed operation.
  • The hash binds together vault identity (device/inode), canonical JSON of the plan (sorted keys, UTF‑8), and projected file writes (path, mode, content hashes).
  • The approval bundle uses schema version claude-obsidian.plan-approval.v3 for forward compatibility.
  • At runtime, hmac.compare_digest verifies the stored hash against a live recomputation; any deviation triggers a TransactionValidationError.

Frequently Asked Questions

What is the purpose of the plan_approval_sha256 function?

plan_approval_sha256 generates a cryptographic commitment to a specific set of file operations. By hashing the vault identity, operation logic, and exact byte-level changes, it creates an unforgeable token that proves the user reviewed exactly these changes before execution.

How does claude-obsidian prevent replay attacks using the canonical hash?

The hash includes SHA‑256 digests of the original and new file contents, file modes, and the vault’s inode-based identity. If an attacker attempts to replay the approval token against different file contents or a copied vault, the recomputed hash diverges from the stored approval_sha256, causing apply_bundle to reject the operation with a TransactionValidationError.

What happens if the vault files change between inspection and application?

If any target file is modified, renamed, or permission-changed after inspection but before application, the prepared writes projection no longer matches the current disk state. When apply_bundle recomputes the canonical hash, the mismatch is detected via hmac.compare_digest, and the transaction aborts, forcing a fresh review.

Which schema version does the approval bundle use?

The approval bundle explicitly declares schema "claude-obsidian.plan-approval.v3" inside the hashed JSON object. This identifier, located in claude_obsidian/transaction.py (lines 1087–1095), ensures that future versions of the tool can distinguish legacy approvals from current ones and maintain backward compatibility or enforce migrations as needed.

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 →