Skip to content
JavaScript Basics: Inspect Shared File Identity with Hard Links

JavaScript Basics: Inspect Shared File Identity with Hard Links

October 27, 2026

Two filesystem names can refer to the same underlying file. Node’s link creates a hard link, and stat can confirm the shared identity. An isolated fixture shows that writing through one name changes what the other reads, while unlinking one name leaves the other usable.

Prepare a scratch directory

Tested on Linux with Node.js v24.19.0. You need the named runtime, Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. The .mjs extension selects ES modules. Assertions check the demonstrated behavior; no npm packages or remote services are needed.

Keep using the same terminal. Stop if setup fails, before saving the source:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Run the example

Save this as demonstrate-hard-links.mjs:

import assert from 'node:assert/strict';
import { mkdtemp, writeFile, readFile, link, stat, unlink, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';

const directory = await mkdtemp(path.join(tmpdir(), 'hard-link-demo-'));
try {
  const original = path.join(directory, 'original.txt');
  const alias = path.join(directory, 'alias.txt');
  await writeFile(original, 'first\n');
  await link(original, alias);
  const a = await stat(original, { bigint: true });
  const b = await stat(alias, { bigint: true });
  assert.equal(a.dev, b.dev);
  assert.equal(a.ino, b.ino);
  assert.equal(a.nlink, 2n);
  console.log('Shared device and inode:', a.dev === b.dev && a.ino === b.ino);
  console.log('Link count:', a.nlink.toString());
  await writeFile(alias, 'updated\n');
  assert.equal(await readFile(original, 'utf8'), 'updated\n');
  console.log('Original reads:', (await readFile(original, 'utf8')).trim());
  await unlink(original);
  assert.equal((await stat(alias, { bigint: true })).nlink, 1n);
  assert.equal(await readFile(alias, 'utf8'), 'updated\n');
  console.log('Alias after unlink:', (await readFile(alias, 'utf8')).trim());
  console.log('Remaining link count:', (await stat(alias, { bigint: true })).nlink.toString());
} finally {
  await rm(directory, { recursive: true, force: true });
}

Run it:

node demonstrate-hard-links.mjs

Observed output:

Shared device and inode: true
Link count: 2
Original reads: updated
Alias after unlink: updated
Remaining link count: 1

Read the result

Both names refer to the same device-and-inode pair. The second name increases the link count, and writing through it updates the same file contents. Unlinking the original name removes that directory entry, not the still-linked file. BigInt stat fields avoid losing precision when checking inode values.

Apply it carefully

Hard links are not copies or symbolic links. They generally require the same filesystem and cannot be used as ordinary directory links. The update shown modifies an existing file in place; replacing a pathname by renaming a different file over it can break the shared-identity relationship. Do not use hard links as independent backups, because writes through either name affect shared data. This demonstration runs only inside one owned temporary directory.

The official reference describes the API and its options. The checks above cover the specific fixture, not every possible input or environment.

Clean up

Remove the source and then the empty practice directory:

rm -- demonstrate-hard-links.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If the directory is not empty, inspect the remaining files rather than deleting unknown contents.

Acknowledgements

This post was created with assistance from AI.

Last updated on