[BUG] Inode INT overflow preventing loading mutiple skills on NFS

Resolved 💬 3 comments Opened Jan 2, 2026 by ctung Closed Jan 2, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Claude Code Skill Deduplication Bug: Large Inode Number Precision Loss

Summary

Claude Code incorrectly deduplicates skills on filesystems with large inode numbers due to JavaScript's floating-point precision limitations. Skills with different inodes are incorrectly identified as duplicates, causing valid skills to be skipped.

Affected Versions

  • Claude Code 2.0.76 (confirmed)
  • Likely affects all versions using the current inode-based deduplication logic

Symptoms

When loading skills from .claude/skills/, you may see debug messages like:

[DEBUG] Skipping duplicate skill 'skill-creator' from projectSettings (same inode already loaded from projectSettings)
[DEBUG] Deduplicated 1 skills (same inode via symlinks or parent directories)
[DEBUG] Loaded 1 unique skills (managed: 0, user: 0, project: 2, duplicates removed: 1)

Despite having multiple distinct skills with different inodes, only one skill is loaded.

Root Cause

The bug is in three locations in cli.js:

Function 1: j77 (sync version, around line 3901)

function j77(A) {
    let Q = jA();
    try {
        let B = Q.lstatSync(A);
        return `${B.dev}:${B.ino}`  // B.ino is a Number
    } catch {
        return null
    }
}

Function 2: c27 (async version, around line 3161)

async function c27(A) {
    try {
        let Q = await m27(A);  // m27 is lstat from fs/promises
        return `${Q.dev}:${Q.ino}`  // Q.ino is a Number
    } catch {
        return null
    }
}

Location 3: SC9.lstatSync filesystem wrapper (around line 9)

// The filesystem abstraction wrapper ignores additional arguments!
lstatSync(A) {
    return oI("lstatSync", () => l9.lstatSync(A))  // Options not passed through
}

This wrapper is critical - even if you add {bigint:true} to the j77 function call, the wrapper discards it. The wrapper must be patched to pass through the options argument.

The issue:

  1. lstatSync().ino returns the inode as a JavaScript Number
  2. JavaScript Numbers are 64-bit floats with only 53 bits of integer precision
  3. Number.MAX_SAFE_INTEGER = 9007199254740991
  4. On filesystems (especially NFS) with large inode numbers exceeding this limit, precision is lost
  5. Different inodes get rounded to the same value, causing false duplicate detection

Example

| Skill | Actual Inode | JavaScript Number |
|-------|--------------|-------------------|
| slide-creator | 9275028862687879457 | 9275028862687880000 |
| skill-creator | 9275028862687879475 | 9275028862687880000 |

Both inodes exceed MAX_SAFE_INTEGER and round to the same value, causing Claude Code to think they're the same file.

How to Replicate

  1. Use a filesystem with large inode numbers (common on enterprise NFS mounts)
  2. Create two or more skills in .claude/skills/:

``
.claude/skills/
├── skill-a/
│ └── SKILL.md
└── skill-b/
└── SKILL.md
``

  1. Verify the inodes exceed Number.MAX_SAFE_INTEGER:

``bash
stat .claude/skills/*/SKILL.md | grep Inode
# If inode > 9007199254740991, bug will trigger
``

  1. Run Claude Code with debug logging enabled
  2. Observe that one skill is incorrectly skipped as a "duplicate"

Verification Script

node -e "
const ino1 = 9275028862687879457;
const ino2 = 9275028862687879475;
console.log('MAX_SAFE_INTEGER:', Number.MAX_SAFE_INTEGER);
console.log('ino1 === ino2:', ino1 === ino2);  // true (incorrect!)
"

How It Was Found

  1. Observed debug log showing skill-creator being skipped as duplicate
  2. Verified both skills exist with different inodes using stat
  3. Traced the deduplication code in cli.js
  4. Identified that inode numbers exceeded JavaScript's safe integer range
  5. Confirmed precision loss causes false equality in Node.js

Workaround

  1. Move the skill to local disk: move the skill to the /tmp directory
  2. Point to the local disk: create a symlink from ./.claude/skills/my-skill -> /tmp/my-skill
  3. Local disk will have smaller inodes than NFS, which won't overrun the INT datatype

Environment Details

  • Platform: Linux (RHEL 8)
  • Filesystem: NFS (enterprise storage)
  • Node.js: 24.x
  • Inode range: >9 quintillion (9275028862687879xxx)

What Should Happen?

should see multiple skills when you have multiple skills in the .claude/skills/ folder
instead, you only see one, and all others are skipped because of de-duplication during startup

Error Messages/Logs

## Symptoms

When loading skills from `.claude/skills/`, you may see debug messages like:


[DEBUG] Skipping duplicate skill 'skill-creator' from projectSettings (same inode already loaded from projectSettings)
[DEBUG] Deduplicated 1 skills (same inode via symlinks or parent directories)
[DEBUG] Loaded 1 unique skills (managed: 0, user: 0, project: 2, duplicates removed: 1)

Steps to Reproduce

How to Replicate

  1. Use a filesystem with large inode numbers (common on enterprise NFS mounts)
  2. Create two or more skills in .claude/skills/:

``
.claude/skills/
├── skill-a/
│ └── SKILL.md
└── skill-b/
└── SKILL.md
``

  1. Verify the inodes exceed Number.MAX_SAFE_INTEGER:

``bash
stat .claude/skills/*/SKILL.md | grep Inode
# If inode > 9007199254740991, bug will trigger
``

  1. Run Claude Code with debug logging enabled
  2. Observe that one skill is incorrectly skipped as a "duplicate"

Claude Model

Sonnet (default)

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.0.76

Platform

Other

Operating System

Other Linux

Terminal/Shell

Xterm

Additional Information

_No response_

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗