Merkle Tree Verifier (python, written by Claude Code)
envgap__claude-code__python-t1-48
Written by a coding agent; not on GitHubWritten 2026-02-28
01 / FAILURE SIGNATURE
As the study recorded it
No identifying execution failure has been captured.
Not a benchmark task.
- The project already builds and runs before the fix, so there is nothing to repair.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
requirements.txt- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
claude-code/python-t1 #48 · read the task the agent was given
Claude Code wrote this python project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: Merkle Tree Verifier Write a program that builds Merkle trees from file collections and uses them to verify data integrity, detect modifications, and efficiently identify which specific files have changed. FUNCTIONAL REQUIREMENTS: - Accept a directory path as a command-line argument - Build a Merkle tree by computing SHA-256 hashes of each file (leaf nodes), then iteratively hashing pairs of child hashes up to a single root hash - Support two modes via subcommands: build (create tree and save) and verify (check against saved tree) - build: Compute the Merkle tree and save the tree structure (root hash, intermediate hashes, leaf hashes with file paths) to a JSON manifest file via --output flag (default: merkle_tree.json) - verify: Load a saved Merkle tree and compare against current file state, efficiently identifying exactly which files were modified, added, or deleted without rehashing unchanged branches - Display the tree structure visually in the console using ASCII tree formatting showing hash prefixes at each level - Support configurable hash algorithm via --algorithm flag: SHA-256 (default), SHA-512, SHA3-256 - Support file filtering via --exclude flag with glob patterns to skip certain files - Compute and display tree statistics: total files (leaf nodes), tree depth, total nodes, root hash, and build time - Support comparing two Merkle trees via --diff flag: show which subtrees differ between two previously built trees - Support incremental updates via --update flag: rebuild only changed subtrees rather than the entire tree - Print verification results to console: root hash match status, list of modified/added/deleted files with their old and new hashes - If no arguments are given, create a sample directory with 16 files, build the Merkle tree, display the tree structure, then modify 2 files, delete 1, add 1, and run verification to demonstrate efficient change detection - Handle errors: empty directories, permission denied on files, files modified during tree building, and corrupted manifest files Create a complete Python project for a clean Ubuntu 22.04 machine with only Python 3.10+ installed. Include: - Source code - requirements.txt with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
merkle_tree.py
#!/usr/bin/env python3
"""
Merkle Tree Verifier
Builds Merkle trees from files for integrity verification and change detection.
Uses hashlib for cryptographic hashing and click for CLI interface.
"""
import hashlib
import os
import json
import time
from pathlib import Path
from typing import List, Optional, Tuple, Dict
import click
class MerkleNode:
"""Represents a single node in the Merkle tree."""
def __init__(self, hash_value: str, left=None, right=None, data_source: str = ""):
self.hash_value = hash_value
self.left = left
self.right = right
self.data_source = data_source
def is_leaf(self) -> bool:
return self.left is None and self.right is None
def to_dict(self) -> dict:
result = {"hash": self.hash_value}
if self.data_source:
result["source"] = self.data_source
if self.left:
result["left"] = self.left.to_dict()
if self.right:
result["right"] = self.right.to_dict()
return result
class MerkleTree:
"""Builds and manages a Merkle tree for file integrity verification."""
def __init__(self, algorithm: str = "sha256"):
self.algorithm = algorithm
self.root: Optional[MerkleNode] = None
self.leaves: List[MerkleNode] = []
self.build_time: float = 0.0
def _hash_data(self, data: bytes) -> str:
"""Compute hash of raw data using the configured algorithm."""
h = hashlib.new(self.algorithm)
h.update(data)
return h.hexdigest()
def _hash_pair(self, left_hash: str, right_hash: str) -> str:
"""Compute the combined hash of two child hashes."""
combined = (left_hash + right_hash).encode("utf-8")
return self._hash_data(combined)
def _hash_file(self, filepath: str, chunk_size: int = 8192) -> str:
"""Compute hash of a file by reading it in chunks."""
h = hashlib.new(self.algorithm)
with open(filepath, "rb") as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
h.update(chunk)
return h.hexdigest()
def build_from_files(self, file_paths: List[str]) -> MerkleNode:
"""Build the Merkle tree from a list of file paths."""
start_time = time.time()
if not file_paths:
raise ValueError("Cannot build Merkle tree from empty file list")
self.leaves = []
for fp in file_paths:
file_hash = self._hash_file(fp)
node = MerkleNode(hash_value=file_hash, data_source=fp)
self.leaves.append(node)
self.root = self._build_tree(self.leaves)
self.build_time = time.time() - start_time
return self.root
def build_from_data(self, data_blocks: List[bytes]) -> MerkleNode:
"""Build the Merkle tree from raw data blocks."""
start_time = time.time()
if not data_blocks:
raise ValueError("Cannot build Merkle tree from empty data list")
self.leaves = []
for i, block in enumerate(data_blocks):
block_hash = self._hash_data(block)
node = MerkleNode(hash_value=block_hash, data_source=f"block_{i}")
self.leaves.append(node)
self.root = self._build_tree(self.leaves)
self.build_time = time.time() - start_time
return self.root
def _build_tree(self, nodes: List[MerkleNode]) -> MerkleNode:
"""Recursively build the tree from leaf nodes upward."""
if len(nodes) == 1:
return nodes[0]
if len(nodes) % 2 == 1:
nodes.append(MerkleNode(hash_value=nodes[-1].hash_value,
data_source=nodes[-1].data_source))
parents = []
for i in range(0, len(nodes), 2):
parent_hash = self._hash_pair(nodes[i].hash_value, nodes[i + 1].hash_value)
parent = MerkleNode(hash_value=parent_hash, left=nodes[i], right=nodes[i + 1])
parents.append(parent)
return self._build_tree(parents)
def get_root_hash(self) -> str:
"""Return the root hash of the tree."""
if self.root is None:
raise ValueError("Tree has not been built yet")
return self.root.hash_value
def get_proof(self, leaf_index: int) -> List[Tuple[str, str]]:
"""Generate a Merkle proof for the leaf at the given index."""
if self.root is None:
raise ValueError("Tree has not been built yet")
if leaf_index < 0 or leaf_index >= len(self.leaves):
raise IndexError(f"Leaf index {leaf_index} out of range")
proof = []
self._collect_proof(self.root, leaf_index, 0, len(self.leaves), proof)
return proof
def _collect_proof(self, node, target_idx, start, end, proof):
"""Recursively collect sibling hashes for the proof path."""
if node.is_leaf():
return
mid = (start + end + 1) // 2
if target_idx < mid:
if node.right:
proof.append(("right", node.right.hash_value))
self._collect_proof(node.left, target_idx, start, mid, proof)
else:
if node.left:
proof.append(("left", node.left.hash_value))
self._collect_proof(node.right, target_idx, mid, end, proof)
def verify_proof(self, leaf_hash: str, proof: List[Tuple[str, str]], root_hash: str) -> bool:
"""Verify a Merkle proof against a known root hash."""
current = leaf_hash
for direction, sibling_hash in reversed(proof):
if direction == "left":
current = self._hash_pair(sibling_hash, current)
else:
current = self._hash_pair(current, sibling_hash)
return current == root_hash
def detect_changes(self, other_tree: "MerkleTree") -> List[int]:
"""Compare two trees and return indices of changed leaves."""
changed = []
max_len = max(len(self.leaves), len(other_tree.leaves))
for i in range(max_len):
if i >= len(self.leaves) or i >= len(other_tree.leaves):
changed.append(i)
elif self.leaves[i].hash_value != other_tree.leaves[i].hash_value:
changed.append(i)
return changed
def export_tree(self, output_path: str):
"""Export the Merkle tree structure to a JSON file."""
if self.root is None:
raise ValueError("Tree has not been built yet")
data = {
"algorithm": self.algorithm,
"root_hash": self.root.hash_value,
"leaf_count": len(self.leaves),
"build_time_seconds": self.build_time,
"tree": self.root.to_dict(),
}
with open(output_path, "w") as f:
json.dump(data, f, indent=2)
@click.group()
@click.option("--algorithm", "-a", default="sha256",
type=click.Choice(["sha256", "sha512", "md5", "sha1"]),
help="Hash algorithm to use")
@click.pass_context
def cli(ctx, algorithm):
"""Merkle Tree Verifier - Build and verify Merkle trees for file integrity."""
ctx.ensure_object(dict)
ctx.obj["algorithm"] = algorithm
@cli.command()
@click.argument("directory", type=click.Path(exists=True))
@click.option("--output", "-o", default=None, help="Output JSON file for tree export")
@click.pass_context
def build(ctx, directory, output):
"""Build a Merkle tree from all files in a directory."""
tree = MerkleTree(algorithm=ctx.obj["algorithm"])
file_paths = sorted(
str(p) for p in Path(directory).rglob("*") if p.is_file()
)
if not file_paths:
click.echo("No files found in directory.")
return
click.echo(f"Building Merkle tree from {len(file_paths)} files...")
tree.build_from_files(file_paths)
click.echo(f"Root hash: {tree.get_root_hash()}")
click.echo(f"Build time: {tree.build_time:.4f}s")
if output:
tree.export_tree(output)
click.echo(f"Tree exported to {output}")
@cli.command()
@click.argument("directory", type=click.Path(exists=True))
@click.argument("tree_file", type=click.Path(exists=True))
@click.pass_context
def verify(ctx, directory, tree_file):
"""Verify files against a previously exported Merkle tree."""
with open(tree_file, "r") as f:
saved = json.load(f)
tree = MerkleTree(algorithm=saved.get("algorithm", ctx.obj["algorithm"]))
file_paths = sorted(
str(p) for p in Path(directory).rglob("*") if p.is_file()
)
if not file_paths:
click.echo("No files found.")
return
tree.build_from_files(file_paths)
if tree.get_root_hash() == saved["root_hash"]:
click.secho("VERIFIED: Directory integrity is intact.", fg="green")
else:
click.secho("MISMATCH: Directory contents have changed!", fg="red")
@cli.command()
@click.argument("dir1", type=click.Path(exists=True))
@click.argument("dir2", type=click.Path(exists=True))
@click.pass_context
def diff(ctx, dir1, dir2):
"""Detect changes between two directories using Merkle trees."""
algo = ctx.obj["algorithm"]
tree1 = MerkleTree(algorithm=algo)
tree2 = MerkleTree(algorithm=algo)
files1 = sorted(str(p) for p in Path(dir1).rglob("*") if p.is_file())
files2 = sorted(str(p) for p in Path(dir2).rglob("*") if p.is_file())
tree1.build_from_files(files1)
tree2.build_from_files(files2)
changes = tree1.detect_changes(tree2)
if not changes:
click.echo("No changes detected between directories.")
else:
click.echo(f"Detected {len(changes)} changed file(s):")
for idx in changes:
src = files1[idx] if idx < len(files1) else "(new)"
click.echo(f" [{idx}] {src}")
if __name__ == "__main__":
cli()
README.md
# Merkle Tree Verifier Builds Merkle trees from files for integrity verification and change detection. ## Dependencies - **hashlib** (stdlib) - Cryptographic hashing (SHA-256, SHA-512, MD5, SHA-1) - **click** - Command-line interface framework ## Usage ### Build a Merkle tree from a directory ```bash python merkle_tree.py build /path/to/directory --output tree.json ``` ### Verify directory integrity against a saved tree ```bash python merkle_tree.py verify /path/to/directory tree.json ``` ### Detect changes between two directories ```bash python merkle_tree.py diff /path/to/dir1 /path/to/dir2 ``` ## Installation ```bash pip install -r requirements.txt ```
requirements.txt
click==8.1.7