cosmiconfig vs lilconfig vs conf: Configuration Loading in Node.js (2026)
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 supportconfigFileswith dynamic import
Download Trends
| Package | Weekly Downloads | Use Case | YAML | TypeScript Config | Persistent Storage |
|---|---|---|---|---|---|
cosmiconfig | ~80M | Config discovery | ✅ | ✅ | ❌ |
lilconfig | ~80M | Config discovery | ❌ (opt-in) | ✅ | ❌ |
conf | ~2M | App settings | N/A | N/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
| Feature | cosmiconfig | lilconfig | conf |
|---|---|---|---|
| Config file discovery | ✅ | ✅ | ❌ |
| YAML support | ✅ Built-in | ❌ Optional | N/A |
| TypeScript configs | ✅ | ✅ | N/A |
| Persistent storage | ❌ | ❌ | ✅ |
| JSON Schema validation | ❌ | ❌ | ✅ |
| Encryption | ❌ | ❌ | ✅ |
| Cross-platform paths | N/A | N/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
dotenvort3-envinstead - 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 →