#362·ni

feat: discover `.nirc` by walking up from cwd

Author: unrevised6419Created Aug 27, 2026Updated Aug 27, 2026

Describe the feature

Today .nirc is looked up at exactly one location. From src/config.ts:

typescript
const customRcPath = process.env.NI_CONFIG_FILE
const home = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME
const defaultRcPath = path.join(home || '~/', '.nirc')
const rcPath = customRcPath || defaultRcPath

There is no directory traversal, so a .nirc sitting next to a project's package.json is ignored. The only way to get per-project configuration today is to export NI_CONFIG_FILE yourself, which has to be done per shell session and is easy to forget or to leak into unrelated projects.

This is inconsistent with the rest of ni: detect() already walks up from cwd to find the lockfile, so the agent is resolved relative to the project while the configuration that shapes how that agent is invoked is resolved relative to the user's home directory.

Suggested solution

Give .nirc the same upward discovery that other package manager config files get, and layer the results.

#359 already introduces exactly this traversal for .npmrc / .yarnrc.yml in src/catalog/range-prefix.ts:

typescript
// Nearest config first, falling back to the one in the home directory, which is
// how both npm and yarn layer their own configuration files.
function findConfigFiles(cwd: string, fileName: string): string[] {
  const found: string[] = []
  let dir = path.resolve(cwd)
  while (true) {
    const filePath = path.join(dir, fileName)
    if (fs.existsSync(filePath))
      found.push(filePath)
    const parent = path.dirname(dir)
    if (parent === dir)
      break
    dir = parent
  }

  const homeConfig = path.join(os.homedir(), fileName)
  if (!found.includes(homeConfig) && fs.existsSync(homeConfig))
    found.push(homeConfig)

  return found
}

The proposal is to reuse that helper (extracted to a shared module) for .nirc, and merge the files it returns key-by-key with the file nearest to cwd winning. ~/.nirc then behaves as a base layer that a project .nirc overrides only for the keys it actually sets, which is how npm and yarn layer their own configuration.

Resolution order would become:

  1. built-in defaults
  2. ~/.nirc
  3. any .nirc found walking up from cwd, nearest last
  4. NI_* environment variables

Backwards compatibility:

  • NI_CONFIG_FILE, when set, keeps its current meaning: that single file is used and no traversal happens.
  • A user with only ~/.nirc sees no behaviour change.
  • Environment variables keep overriding files.

Two details worth settling in the discussion:

  • getConfig() currently takes no arguments and caches its result in a module-level variable, while rcPath is computed once at import time. Traversal needs a cwd, so getConfig() would read process.cwd() (or accept an optional cwd) and the cache key would have to account for it.
  • getConfig() ends by overwriting defaultAgent with the result of detect(), so a project-level defaultAgent would still lose to lockfile detection. That may well be the desired precedence, but it is worth stating explicitly rather than leaving it as an accident of ordering.

Alternative

  • Nearest .nirc wins outright and ~/.nirc is ignored when a closer file exists. Simpler, but a project file that sets one key would silently discard every global setting.
  • Document NI_CONFIG_FILE with direnv or a similar tool as the supported per-project story and change nothing in ni. This works but pushes the problem onto every user and every project.