Skip to main content

cosmiconfig vs lilconfig vs conf: Configuration Loading in Node.js (2026)

·PkgPulse Team

TL;DR

cosmiconfig is the standard for tool configuration discovery — it's what ESLint, Prettier, Babel, PostCSS, and hundreds of other tools use to find config files (.eslintrc, .prettierrc, eslint.config.js, etc.). lilconfig is cosmiconfig's lighter replacement — same API, no YAML support by default, 30% smaller. conf is for a different use case — persistent user preferences for CLI apps and desktop tools, stored in the OS's app data directory with JSON Schema validation and optional encryption. For finding project config files: lilconfig. For storing user settings between runs: conf.

Key Takeaways

  • cosmiconfig: ~80M weekly downloads — the config discovery standard (ESLint/Prettier/Babel use it)
  • lilconfig: ~80M weekly downloads — drop-in replacement, no yaml dependency, slightly smaller
  • conf: ~2M weekly downloads — persistent user settings, not config file discovery
  • cosmiconfig and lilconfig solve "find the user's config for my tool" — lilconfig is now preferred
  • conf solves "store user preferences between CLI runs" — completely different use case
  • For TypeScript config files (.eslintrc.ts), both support configFiles with dynamic import

PackageWeekly DownloadsUse CaseYAMLTypeScript ConfigPersistent Storage
cosmiconfig~80MConfig discovery
lilconfig~80MConfig discovery❌ (opt-in)
conf~2MApp settingsN/AN/A

cosmiconfig

cosmiconfig — the config discovery standard:

How config discovery works

cosmiconfig searches for config in this order (example for "mytool"):
1. package.json → "mytool" key
2. .mytoolrc              (JSON or YAML)
3. .mytoolrc.json
4. .mytoolrc.yaml / .mytoolrc.yml
5. .mytoolrc.js / .mytoolrc.cjs / .mytoolrc.mjs
6. .mytoolrc.ts / .mytoolrc.cts / .mytoolrc.mts
7. mytool.config.js / mytool.config.cjs / mytool.config.mjs
8. mytool.config.ts / mytool.config.cts / mytool.config.mts

Searches in current directory, then parent directories, up to root.

Basic usage

import { cosmiconfig } from "cosmiconfig"

// Find config for "pkgpulse" tool:
const explorer = cosmiconfig("pkgpulse")

// Search from current directory up:
const result = await explorer.search()

if (result) {
  console.log(result.config)    // The loaded config object
  console.log(result.filepath)  // "/path/to/project/.pkgpulserc.json"
  console.log(result.isEmpty)   // true if file was empty
} else {
  console.log("No config found — using defaults")
}

// Load from a specific path:
const specific = await explorer.load("/path/to/pkgpulse.config.js")

Config file examples

// .pkgpulserc.json
{
  "threshold": 80,
  "ignore": ["@types/*", "eslint-*"],
  "reportFormat": "json",
  "autoFix": true
}
# .pkgpulserc.yaml
threshold: 80
ignore:
  - "@types/*"
  - "eslint-*"
reportFormat: json
autoFix: true
// pkgpulse.config.js
export default {
  threshold: 80,
  ignore: ["@types/*", "eslint-*"],
  reportFormat: "json",
  autoFix: true,
}
// pkgpulse.config.ts
import type { PkgPulseConfig } from "@pkgpulse/sdk"

const config: PkgPulseConfig = {
  threshold: 80,
  ignore: ["@types/*", "eslint-*"],
  reportFormat: "json",
  autoFix: true,
}

export default config
// package.json — "pkgpulse" key:
{
  "name": "my-project",
  "pkgpulse": {
    "threshold": 80,
    "ignore": ["@types/*"]
  }
}

Custom loaders

import { cosmiconfig } from "cosmiconfig"
import { parse } from "smol-toml"
import { readFile } from "fs/promises"

// Add TOML support:
const explorer = cosmiconfig("mytool", {
  loaders: {
    ".toml": async (filepath) => {
      const content = await readFile(filepath, "utf8")
      return parse(content)
    },
  },
  searchPlaces: [
    "package.json",
    ".mytoolrc",
    ".mytoolrc.json",
    ".mytoolrc.yaml",
    ".mytoolrc.toml",  // Add TOML
    "mytool.config.js",
    "mytool.config.ts",
  ],
})

Cache and sync API

import { cosmiconfig, cosmiconfigSync } from "cosmiconfig"

// Sync version (for build tools where async isn't available):
const explorerSync = cosmiconfigSync("mytool")
const result = explorerSync.searchSync()

// Caching — cosmiconfig caches results by default:
const explorer = cosmiconfig("mytool")

// First call — reads file system:
const result1 = await explorer.search()

// Subsequent calls — from cache:
const result2 = await explorer.search()  // No file I/O

// Clear cache if files might have changed:
explorer.clearCaches()

lilconfig

lilconfig — the lighter cosmiconfig alternative:

Why lilconfig?

// lilconfig removes the yaml dependency from cosmiconfig:
// cosmiconfig: ~400KB (includes js-yaml)
// lilconfig:   ~100KB (no yaml by default)

// API is 1:1 compatible with cosmiconfig:
import { lilconfig } from "lilconfig"

const explorer = lilconfig("pkgpulse")
const result = await explorer.search()

// Same result object:
// { config, filepath, isEmpty }

Add YAML support to lilconfig

import { lilconfig } from "lilconfig"
import { load as yamlLoad } from "js-yaml"
import { readFile } from "fs/promises"

// Opt-in to YAML (unlike cosmiconfig, not default):
const explorer = lilconfig("pkgpulse", {
  loaders: {
    ".yaml": async (filepath) => {
      const content = await readFile(filepath, "utf8")
      return yamlLoad(content)
    },
    ".yml": async (filepath) => {
      const content = await readFile(filepath, "utf8")
      return yamlLoad(content)
    },
  },
})

Migration from cosmiconfig

// Before:
import { cosmiconfig } from "cosmiconfig"
const explorer = cosmiconfig("mytool")

// After (drop-in replacement):
import { lilconfig } from "lilconfig"
const explorer = lilconfig("mytool")

// No other changes needed for the common case.
// If you use YAML configs, add the yaml loader as shown above.

conf

conf — persistent user settings for CLI tools and apps:

Basic usage

import Conf from "conf"

// Define schema and store:
const store = new Conf({
  projectName: "pkgpulse-cli",
  schema: {
    apiKey: { type: "string" },
    defaultThreshold: { type: "number", default: 80, minimum: 0, maximum: 100 },
    ignoredPackages: {
      type: "array",
      items: { type: "string" },
      default: [],
    },
    outputFormat: {
      type: "string",
      enum: ["json", "table", "minimal"],
      default: "table",
    },
  },
})

// Get and set settings:
store.set("apiKey", "pk_live_abc123")
console.log(store.get("apiKey"))  // "pk_live_abc123"

store.set("defaultThreshold", 85)
console.log(store.get("defaultThreshold"))  // 85

// Delete:
store.delete("apiKey")

// Clear all:
store.clear()

Where conf stores data

import Conf from "conf"

const store = new Conf({ projectName: "pkgpulse-cli" })
console.log(store.path)
// macOS:   ~/Library/Preferences/pkgpulse-cli/config.json
// Linux:   ~/.config/pkgpulse-cli/config.json
// Windows: %APPDATA%\pkgpulse-cli\Config\config.json

// Full config object:
console.log(store.store)
// { apiKey: "pk_live_...", defaultThreshold: 85, ... }

Encrypted secrets

import Conf from "conf"

const store = new Conf({
  projectName: "pkgpulse-cli",
  encryptionKey: process.env.ENCRYPTION_KEY,  // Must be consistent
  schema: {
    apiKey: { type: "string" },
  },
})

// apiKey is stored encrypted — not readable if you open the JSON file
store.set("apiKey", "pk_live_secret123")

CLI workflow (login/logout)

import Conf from "conf"

const store = new Conf({ projectName: "pkgpulse-cli" })

// pkgpulse login command:
export async function login(apiKey: string) {
  // Validate key:
  const response = await fetch("https://api.pkgpulse.com/me", {
    headers: { Authorization: `Bearer ${apiKey}` },
  })

  if (!response.ok) throw new Error("Invalid API key")

  store.set("apiKey", apiKey)
  const { email } = await response.json()
  console.log(`Logged in as ${email}`)
}

// pkgpulse logout command:
export function logout() {
  store.delete("apiKey")
  console.log("Logged out")
}

// pkgpulse check command — use stored API key:
export async function checkPackage(name: string) {
  const apiKey = store.get("apiKey")
  if (!apiKey) throw new Error("Not logged in. Run: pkgpulse login")

  const response = await fetch(`https://api.pkgpulse.com/packages/${name}`, {
    headers: { Authorization: `Bearer ${apiKey}` },
  })

  return response.json()
}

Feature Comparison

Featurecosmiconfiglilconfigconf
Config file discovery
YAML support✅ Built-in❌ OptionalN/A
TypeScript configsN/A
Persistent storage
JSON Schema validation
Encryption
Cross-platform pathsN/AN/A
Bundle size~400KB~100KB~200KB
ESM
CJS

When to Use Each

Choose lilconfig if:

  • Building a CLI tool or framework that needs user-configurable settings
  • You want the cosmiconfig discovery pattern — this is the modern choice
  • YAML config file support is not needed (or you add it explicitly)
  • Slightly smaller bundle than cosmiconfig matters

Choose cosmiconfig if:

  • Existing tool or code already uses it
  • You need YAML out of the box without adding loaders
  • You want the reference implementation with most features

Choose conf if:

  • Building a CLI tool that needs to remember user authentication/preferences
  • API keys, tokens, user settings that persist between runs
  • You want OS-appropriate config paths (not just .config/ in the project directory)
  • JSON Schema validation and optional encryption of sensitive values

Don't use either if:

  • You just need environment variables — use dotenv or t3-env instead
  • You need a single config file in a fixed location — just fs.readFile + JSON.parse

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on cosmiconfig v9.x, lilconfig v3.x, and conf v13.x.

Compare developer tool and configuration packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.