Content-hash entire directory trees. Zero dependencies.
Perfect for CI cache keys, build verification, and change detection.
You need to know if a directory actually changed — not just timestamps, but real content. Whether it's busting a CI cache, verifying a build output matches, or watching a source tree for edits, you need a content-addressable hash of the whole tree.
Existing solutions either pull in heavy dependencies, don't handle cross-platform edge cases (CRLF vs LF), or can't diff two trees.
hashdir does one thing well: hash a directory deterministically by content.
npm install -g hashdir$ hashdir ./src
Hash: a3f8c2e1d4b5...
Algorithm: sha256
Files: 23
Total size: 45.2 KB$ hashdir diff ./build ./dist
Directories differ.
Modified (1):
~ bundle.js
Added (2):
+ sourcemap.js.map
+ manifest.jsonExit code is 1 when directories differ — perfect for CI:
hashdir diff ./expected ./actual || echo "Build output changed!"$ hashdir watch ./src --interval 1000
Watching: /path/to/src (interval: 1000ms, algo: sha256)
[2026-06-02T02:00:00.000Z] CHANGED → b7e3f1a2... (24 files, 47.1 KB)$ hashdir list ./src --ext .js,.ts# GitHub Actions
- name: Get source hash
run: echo "HASH=$(hashdir ./src --json | jq -r .hash)" >> $GITHUB_ENV
- name: Cache
uses: actions/cache@v3
with:
path: ./build
key: build-${{ env.HASH }}| Command | Description |
|---|---|
hash <dir> |
Hash a directory tree (default) |
diff <dirA> <dirB> |
Compare two directories by content |
watch <dir> |
Poll for content changes |
list <dir> |
List files that would be hashed |
| Flag | Description |
|---|---|
-a, --algo <algo> |
Hash algorithm: sha256, sha1, md5, sha512 (default: sha256) |
-i, --ignore <names> |
Comma-separated file/directory names to skip |
-e, --ext <exts> |
Only hash these extensions (e.g. .js,.ts) |
-d, --depth <n> |
Max directory depth |
-n, --normalize |
Normalize text files (CRLF→LF, strip trailing whitespace) |
--dotfiles |
Include dotfiles (hidden files) |
--follow-links |
Follow symbolic links |
--files |
Show per-file hashes in output |
--json |
JSON output |
--markdown |
Markdown output |
-w, --watch |
Watch mode |
Cross-platform builds often produce different files due to line endings. Use --normalize to treat CRLF and LF as identical:
hashdir diff ./build-mac ./build-windows --normalizeThis normalizes line endings, strips trailing whitespace, and ensures a trailing newline before hashing.
const { hashDirectory, diffDirectories, watchDirectory } = require('hashdir');
// Hash a directory
const result = hashDirectory('./src', {
algorithm: 'sha256',
ignore: ['node_modules', '.git'],
extensions: ['.js', '.ts'],
normalize: true,
includePerFile: true,
});
// → { hash: 'a3f8...', fileCount: 23, totalSize: 46284, files: [...] }
// Compare directories
const diff = diffDirectories('./build', './dist');
// → { identical: false, added: [...], removed: [...], modified: [...], unchanged: [...] }
// Watch for changes
const watcher = watchDirectory('./src', (event) => {
if (event.changed) console.log('Content changed:', event.hash);
}, { interval: 2000 });
watcher.stop(); // cleanup- Walk the directory tree (sorted for determinism)
- Read each file's content
- Hash each file individually
- Concatenate
path\0hash\0for all files - Hash the combined string → final tree hash
This means:
- Renaming a file changes the hash (path is part of the input)
- Reordering files doesn't change the hash (sorted)
- Two identical trees always produce the same hash
No npm install black hole. Uses only Node.js built-ins: fs, path, crypto.
MIT