Skip to content
Back to skills

Content Hash Cache Pattern

ASecurity

Cache expensive file-processing results (PDF parsing, OCR, text extraction, image analysis) keyed by a SHA-256 hash of file content rather than file path, so renamed files still hit the cache and changed content auto-invalidates it. Use when building a file-processing pipeline that reprocesses the same files across runs, or when adding a --cache/--no-cache option without touching the existing pure processing function.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 16, 2026
ai-agentsgodebugginggitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add shennawardana23/skillme --skill content-hash-cache-pattern --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Content Hash Cache Pattern?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Content Hash Cache Pattern
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/shennawardana23-content-hash-cache-pattern/badge)](https://www.skillsdirectory.com/skills/shennawardana23-content-hash-cache-pattern)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: content-hash-cache-pattern
description: Cache expensive file-processing results (PDF parsing, OCR, text extraction, image analysis) keyed by a SHA-256 hash of file content rather than file path, so renamed files still hit the cache and changed content auto-invalidates it. Use when building a file-processing pipeline that reprocesses the same files across runs, or when adding a --cache/--no-cache option without touching the existing pure processing function.
license: Apache-2.0
metadata:
  version: "0.1.0"
---

# Content-Hash File Cache Pattern

Cache expensive file-processing results using a SHA-256 hash of the file's *content* as the cache key. Unlike path-based caching, this survives file moves and renames, and auto-invalidates the moment content changes — no separate invalidation logic needed.

## When to Activate

- Building a file-processing pipeline (PDF parsing, OCR, text extraction, image analysis) where the same files reappear across runs
- Processing cost is high enough that reprocessing unchanged files is wasteful
- Adding a `--cache`/`--no-cache` flag to a CLI tool
- Retrofitting caching onto an existing pure function without modifying that function

## Core Pattern (Go)

### 1. Content hash as the cache key

Hash the file in chunks — never load the whole file into memory:

```go
package cache

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"os"
)

func ComputeFileHash(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", fmt.Errorf("open %s: %w", path, err)
	}
	defer f.Close()

	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return "", fmt.Errorf("hash %s: %w", path, err)
	}
	return hex.EncodeToString(h.Sum(nil)), nil
}
```

`io.Copy` streams the file into the hasher in fixed-size buffers, so this scales to large files without loading them fully into memory.

### 2. Cache entry type

```go
type CacheEntry struct {
	FileHash   string   `json:"file_hash"`
	SourcePath string   `json:"source_path"`
	Document   Document `json:"document"` // the cached result
}
```

### 3. File-based storage: `{hash}.json`

O(1) lookup by hash, no separate index file:

```go
package cache

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
)

func WriteEntry(cacheDir string, entry CacheEntry) error {
	if err := os.MkdirAll(cacheDir, 0o755); err != nil {
		return fmt.Errorf("create cache dir: %w", err)
	}
	data, err := json.Marshal(entry)
	if err != nil {
		return fmt.Errorf("marshal cache entry: %w", err)
	}
	path := filepath.Join(cacheDir, entry.FileHash+".json")
	return os.WriteFile(path, data, 0o644)
}

// ReadEntry returns (entry, true) on a cache hit, (zero, false) on a
// miss — including a miss on corrupted or unreadable cache files, so
// corruption degrades to a re-process rather than a crash.
func ReadEntry(cacheDir, fileHash string) (CacheEntry, bool) {
	path := filepath.Join(cacheDir, fileHash+".json")
	data, err := os.ReadFile(path)
	if err != nil {
		return CacheEntry{}, false
	}
	var entry CacheEntry
	if err := json.Unmarshal(data, &entry); err != nil {
		return CacheEntry{}, false
	}
	return entry, true
}
```

### 4. Service-layer wrapper (single responsibility)

Keep the processing function pure. Caching is a separate layer that wraps it:

```go
// ExtractText is pure: no cache awareness, easy to test and reuse.
func ExtractText(path string) (Document, error) {
	// ... real extraction logic ...
	return Document{}, nil
}

type ExtractOptions struct {
	CacheEnabled bool
	CacheDir     string
}

// ExtractWithCache is the service layer: cache check -> extraction -> cache write.
func ExtractWithCache(path string, opts ExtractOptions) (Document, error) {
	if !opts.CacheEnabled {
		return ExtractText(path)
	}

	hash, err := ComputeFileHash(path)
	if err != nil {
		return Document{}, err
	}

	if entry, ok := ReadEntry(opts.CacheDir, hash); ok {
		return entry.Document, nil
	}

	doc, err := ExtractText(path)
	if err != nil {
		return Document{}, err
	}
	entry := CacheEntry{FileHash: hash, SourcePath: path, Document: doc}
	if err := WriteEntry(opts.CacheDir, entry); err != nil {
		// A cache write failure should not fail the caller — the result is
		// still correct, just not persisted for next time.
		return doc, nil
	}
	return doc, nil
}
```

## Key Design Decisions

| Decision | Rationale |
|---|---|
| SHA-256 content hash, not path | Path-independent; auto-invalidates the instant content changes |
| `{hash}.json` file naming | O(1) lookup, no index file to keep in sync |
| Service layer wraps a pure function | Extraction logic stays testable and reusable; caching is orthogonal |
| Corrupted/missing cache file → cache miss | Graceful degradation: reprocess rather than crash |
| Cache write failure → log, don't fail the call | The correct result still reaches the caller even if persistence fails |
| Lazy `MkdirAll` on first write | No setup step required before first use |

## Best Practices

- Hash content, not paths — paths change identity, content doesn't.
- Stream large files into the hasher (`io.Copy`) instead of reading them fully into memory first.
- Keep the processing function pure — it should have zero knowledge that caching exists.
- Log cache hit/miss with a truncated hash prefix for debugging without flooding logs.
- Treat any cache-read error (missing file, bad JSON, wrong shape) as a miss, never as a fatal error.

## Gotchas

- Two files with identical content but different intended outputs (e.g. same PDF processed with two different extraction configs) will collide on the same cache key if the config isn't part of what's hashed — if extraction behavior depends on parameters beyond file bytes, fold those parameters into the key (e.g. hash the file content concatenated with a serialized config) or this pattern silently returns the wrong result for the second config.
- A cache entry for a very large result (e.g. full-page OCR output for a 500-page PDF) written as a single JSON file works but doesn't scale indefinitely — past some size, prefer streaming the cached artifact to disk directly rather than round-tripping it through JSON.
- This pattern is wrong for anything that must always be fresh (a live price feed, a real-time API response) — the entire point of the pattern is durable reuse of a result tied to unchanging content, which doesn't apply to data that's expected to change independent of any file.

## Anti-Patterns to Avoid

```go
// BAD: path-based caching — breaks the moment a file is renamed or moved
cache := map[string]Document{"/path/to/file.pdf": result}

// BAD: caching concern baked into the pure function — now it has two
// responsibilities and can't be tested or reused without a cache
func ExtractText(path string, cacheEnabled bool, cacheDir string) (Document, error) {
    if cacheEnabled { /* ... */ }
    // ...
}
```

## Real-world grounding

Git's object model uses exactly this idea at the core of the entire system: every blob, tree, and commit is addressed by the SHA hash of its own content, not by a file path. That's precisely why renaming a file with unchanged content doesn't create a new object in Git's store — the object is identified by what it contains, not where it lives. This pattern applies the same principle to a processing cache instead of a version-control object store.

## Verification

- [ ] The cache key is a hash of file content, never the file path
- [ ] Large files are hashed via streaming (`io.Copy`), not loaded fully into memory
- [ ] The processing function has no cache-related parameters or branches
- [ ] A missing or corrupt cache file degrades to a cache miss, not a crash
- [ ] Parameters that affect output (beyond file bytes) are folded into the cache key if they vary

Files in this skill

  • SKILL.md7.6 KB
  • evals/evals.json2.7 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.