Integrations

CLI

Find where your code reads each config, and fail the build when it reads a key that does not exist, was renamed, or has the wrong type, with the configdirector command line tool

Introduction

The CLI is in beta. Commands and their output may change before 1.0, and the changelog lists every change.

configdirector checks your code against your ConfigDirector project, on your machine or as a CI step. The beta has two commands for a repository:

  • configdirector scan lists every place your code reads a config key through a ConfigDirector SDK or OpenFeature provider, with the file and line.
  • configdirector validate checks those keys against the project's configs, and fails the build when the code reads a key no config has, the old key of a renamed config, a config as the wrong type, a config that is not served to that kind of SDK, or an enum value the config does not allow.

Both commands only read from ConfigDirector. They never change a config.

If you have used LaunchDarkly's ld-find-code-refs or ConfigCat's configcat scan: those tools search your code for the keys your flags already have. scan also lists the SDK calls whose key no config has, which is how validate catches a misspelled key before it ships. A key held in a constant is found too, with nothing to configure.

Install

The CLI needs Node.js 20 or later. Install it globally:

npm install -g @configdirector/cli@beta
configdirector --version

Or run it without installing by writing npx @configdirector/cli@beta in place of configdirector, as the CI examples do:

npx @configdirector/cli@beta --version

@beta gets the newest beta. To upgrade on your own schedule, replace beta with a version number from the changelog.

Create an API token

The CLI reads your projects and configs through the Admin API, with an API token.

  1. In the ConfigDirector dashboard, open API tokens and create a token.
  2. Choose Projects: Read and Configs: Read. They are all the CLI needs, and a token with only these permissions cannot change anything.
  3. Set an expiration on a token you keep on your workstation, and do not commit a token into source control.

A token belongs to the member who created it, and it stops working when that member leaves the organization. For CI, create a token used only by CI rather than reusing the one on your workstation, so you can replace or revoke it without affecting anything else. If its creator leaves, CI fails with API_TOKEN_OWNER_REMOVED until another member creates a new one.

Configure

Sign in on your machine

configdirector login

login asks you to paste the token and hides it while you do. It checks that the token can list your projects, then saves it where only your user can read it:

SystemSaved in
macOS~/Library/Application Support/configdirector/config.json
Linux~/.config/configdirector/config.json
Windows%APPDATA%\configdirector\config.json

When XDG_CONFIG_HOME is set, the token is saved in $XDG_CONFIG_HOME/configdirector/config.json instead. configdirector login --token <token> saves a token without asking, and running login again replaces the saved token.

In CI, skip login and set CONFIGDIRECTOR_TOKEN. The CLI uses --token first, then CONFIGDIRECTOR_TOKEN, then the token login saved.

From the repository root, with your project's slug, shown as Project slug on the project's Project details page:

configdirector init --project web-app

init checks that the project exists and writes configdirector.json in the current directory. It never overwrites an existing file. Commit the file, so every run and every CI job uses the same project, and run scan and validate from the directory that holds it.

configdirector.json
{
  "project": "web-app",
  "include": ["src/**"],
  "exclude": ["src/generated/**"],
  "ignoreKeys": ["billing-flag-from-other-project"]
}
FieldMeaning
projectThe project's slug. --project and CONFIGDIRECTOR_PROJECT take precedence over it.
includeGlobs of the files to scan, relative to configdirector.json. init writes ["**"], every file.
excludeGlobs of the files to skip, on top of the files the scan always skips.
ignoreKeysKeys that validate never reports, such as the key of a config another project serves.

Options and environment variables

Every command takes these options, as well as --help and --version:

OptionEnvironment variableMeaning
--token <token>CONFIGDIRECTOR_TOKENThe API token. Without either, the token saved by login.
--project <slug>CONFIGDIRECTOR_PROJECTThe project's slug. Without either, scan and validate use the project in configdirector.json.
--base-url <url>CONFIGDIRECTOR_BASE_URLThe Admin API host. Defaults to https://api.configdirector.com.
--jsonPrint JSON instead of text. See Output.

An option takes precedence over its environment variable. An empty variable counts as unset, so a CI job whose token secret is missing stops with No API token found rather than sending an empty token.

Scan

configdirector scan

scan lists every place your code reads a config key, grouped by key, with the file, the line, the SDK call and the SDK:

max-cart-items
  src/admin/feature-list.ts:1 (indirect)
  src/checkout.ts:6 getValue (js-server)

new-checkout
  src/admin/feature-list.ts:1 (indirect)
  src/checkout.ts:5 getValue(NEW_CHECKOUT) (js-server)
  test/checkout.test.ts:3 createTestClient (js-server, test)

Dynamic references (calls whose key is not a string literal)
  src/settings.ts:3 getValue(setting.key) (js-server)

3 direct, 2 indirect and 1 dynamic references in 5 scanned files.

Each place is one of three kinds of code reference:

  • Direct: an SDK call whose key is known, either a string such as getValue("max-cart-items", 20) or a constant that holds one, shown after the call as getValue(NEW_CHECKOUT). These are the references validate checks.
  • Indirect: one of the project's keys written as a quoted string anywhere else, such as a list of keys, the argument of your own wrapper function, or a test fixture. The key is used there, but not through a call validate can check, so look at these before you rename or archive the config.
  • Dynamic: an SDK call whose key cannot be known from the code, such as a variable, a key built at runtime, or a constant assigned a different string in two places. These are listed so you know they exist. They are never checked, so they never fail the build; to have one checked, pass the key as a string or a constant.

A reference is marked test when it is a call to an SDK test client, such as createTestClient or setValue, or an indirect reference in a test directory or a test file such as checkout.test.ts.

The scan exits with 0 whenever it ran, whether it found references or not. configdirector scan src scans only src.

What the scan reads

A call is found when its file imports a ConfigDirector SDK or the OpenFeature SDK. In JavaScript, TypeScript, Python, Dart and Go, it is also found when the file imports one of your own files that does, such as the setup module the SDK pages create. In Swift and Go, it is enough for another file of the same target or package to import the SDK, and Nuxt's auto-imported composables need no import. A getValue from another library in a file that imports none of these is not reported.

The scan skips the files .gitignore ignores, the node_modules, vendor, build, dist and target directories, binary files, and files over 1 MB. include and exclude in configdirector.json narrow it further.

If a call you expected is missing, the file most likely reaches the SDK client in one of these ways:

  • Through two of your own files, such as a module that imports the setup module and passes the client on. Import the setup module directly, or have the module in between re-export the client with export { client } from "./setup".
  • Through a path alias that only your bundler knows, such as resolve.alias in vite.config.ts. Add the alias to compilerOptions.paths in tsconfig.json.
  • Through a CommonJS re-export, such as module.exports = require("./setup").
  • In Kotlin, Java or C#, through a class of your own, in a file that never names the SDK. Declare the client with its SDK type, such as a ConfigDirectorClient parameter or property, so the file names the SDK.

Validate

configdirector validate

validate runs the same scan, then checks every direct reference against the project's configs. It lists the problems it found by kind, and exits with 1 when there is at least one:

Unknown keys
  src/checkout.ts:9 free-shiping: no config in web-app has this key.

Deprecated keys
  src/checkout.ts:6 checkout-v2: the config was renamed to new-checkout.

Type mismatches
  src/checkout.ts:8 checkout-timeout: read as string, but the config's type is timespan, served as a float.

Availability mismatches
  src/theme.tsx:4 fraud-threshold: read from a client SDK (react), but the config is server-only.

Enum values not allowed
  src/theme.tsx:3 theme: "system" is not one of the allowed values: light, dark.

Checked 7 direct references in 4 scanned files and found 5 problems. 1 dynamic reference was not checked.
ProblemWhat it means for your appWhat to do
Unknown keysNo config in the project has this key, as its current key or an old one, so the SDK returns the in-code default value every time. The key of an archived config is unknown too.Correct the key, or create the config. If another project serves the key, add it to ignoreKeys.
Deprecated keysThe config was renamed, and the code still reads an old key. The old key works until ConfigDirector removes it once it is no longer used, as Key explains.Replace it with the current key, which the output names.
Type mismatchesThe in-code default value, or a getter whose name sets the type such as getBoolean, reads the config as a type the SDKs do not serve it as, so the SDK can return the in-code default value instead of the config's value.Read the config as the type the output names.
Availability mismatchesA client SDK reads a server-only config, or a server SDK reads a client-only one. That SDK never receives the config, so it returns the in-code default value every time.Change the config's SDK availability, or read the config where it is served.
Enum values not allowedThe in-code default value is not one of the enum's allowed values.Use an allowed value as the in-code default value, or add the value to the enum.

The SDKs serve a timespan as a number in its unit and a URL as a string. So an in-code default value, or a getter whose name sets the type, reads a config of these types: a boolean reads a boolean config; an integer reads an integer, float or timespan config; a decimal number reads a float or timespan config; a string reads a string, enum or URL config; and an object or array reads a JSON config. Read a timespan with a number, not a string such as "30s". An enum whose values are numbers can also be read with a number.

A test client call is checked for unknown and deprecated keys only, since ConfigDirector never serves a test client. Indirect and dynamic references are never checked, and a config's lifecycle state is never a problem.

Exit codes

CodeMeaning
0The command succeeded. For validate, it found no problem.
1validate found at least one problem.
2The command could not run: a mistake in the command, no token or a rejected one, a missing permission, a suspended service, or an unreachable API. The message says which.

A CI job can therefore tell a wrong config key from an outage.

Run it in CI

Add the token as a secret named CONFIGDIRECTOR_TOKEN, then run validate with npx.

On GitHub, add the secret under the repository's Settings → Secrets and variables → Actions, and add this workflow, or its last step to an existing job:

.github/workflows/configdirector.yml
name: ConfigDirector

on:
  pull_request:
  push:
    branches: [main]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v6
        with:
          node-version: 24
      - run: npx --yes @configdirector/cli@beta validate
        env:
          CONFIGDIRECTOR_TOKEN: ${{ secrets.CONFIGDIRECTOR_TOKEN }}

On GitLab, add the token as a masked variable under the project's Settings → CI/CD → Variables, which GitLab passes to every job as an environment variable, and add this job to .gitlab-ci.yml. A variable marked Protect variable reaches only pipelines of protected branches and tags, so leave it unmarked for the check to run on merge requests.

.gitlab-ci.yml
configdirector-validate:
  image: node:24
  script:
    - npx --yes @configdirector/cli@beta validate

Each scan or validate run makes two Admin API requests, and login and init make one each. They count toward your organization's monthly free allowance like any other Admin API request.

Output

Every command prints text for people by default. With --json, it prints JSON instead, for scripts and CI. Errors and the token prompt go to stderr, so with --json the JSON is all that reaches stdout:

configdirector scan --json | jq -r '.references[] | select(.key == "new-checkout") | "\(.file):\(.line)"'

validate --json prints the scan's JSON with a findings array, and still exits with 1 when the array is not empty. Shortened to one reference of each kind and one finding:

{
  "project": "web-app",
  "filesScanned": 4,
  "references": [
    {
      "key": "new-checkout",
      "file": "src/checkout.ts",
      "line": 5,
      "kind": "direct",
      "sdk": "js-server",
      "call": "getValue",
      "argument": "NEW_CHECKOUT",
      "default": { "type": "boolean", "value": false },
      "test": false
    },
    {
      "key": "new-checkout",
      "file": "src/flags.ts",
      "line": 1,
      "kind": "indirect",
      "test": false
    }
  ],
  "dynamicReferences": [
    {
      "file": "src/theme.tsx",
      "line": 5,
      "sdk": "react",
      "call": "useConfigValue",
      "argument": "setting.key"
    }
  ],
  "findings": [
    {
      "kind": "availability-mismatch",
      "key": "fraud-threshold",
      "file": "src/theme.tsx",
      "line": 4,
      "sdk": "react",
      "sdkType": "client",
      "configAvailableTo": ["server"]
    }
  ]
}

A direct reference has argument when its key came from a constant, default when the call passes an in-code default value, and getterType when the getter's name sets the type. Each finding has a kind, the key, the file and the line, and the config facts it was checked against:

kindAlso has
unknown-key
deprecated-keycurrentKey
type-mismatchconfigType, servedAs, and default, getterType or both
availability-mismatchsdk, sdkType, configAvailableTo
enum-value-not-alloweddefault, allowedValues

The JSON Schema in the package describes every field.