HarperFast / HarperFast/harper

Docs and code disagree on HARPER_SET_CONFIG removal: docs say the key is deleted, 5.1.22 restores the pre-SET_CONFIG original

Open
#1,953 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
89
Forks
10
Avg merge
2d 2h
Merged PRs (30d)
205

Description

## Summary

The documented removal semantics for `HARPER_SET_CONFIG` are the **opposite** of what 5.1.22 ships.

The docs say a key removed from `HARPER_SET_CONFIG` is **deleted** from the config. The code **restores** the value the key had before `HARPER_SET_CONFIG` first forced it — `HARPER_SET_CONFIG` is in the same restore branch as `HARPER_CONFIG` and `HARPER_DEFAULT_CONFIG`.

Either the docs or the code is wrong; I can't tell which is intended, so I'm reporting the contradiction rather than proposing a fix.

## The documentation

, under **`HARPER_SET_CONFIG` → At runtime**, verbatim:

> - Always overrides all other configuration sources
> - Takes precedence over user edits, file values, `HARPER_CONFIG`, and `HARPER_DEFAULT_CONFIG`
> - **When a key is removed from the variable, it is deleted from the config (not restored)**

That is stated in deliberate contrast to the other two on the same page:

> `HARPER_CONFIG`: When a key is removed from the variable, its original value is restored (or the key is deleted if `HARPER_CONFIG` introduced it)

> `HARPER_DEFAULT_CONFIG`: When a key is removed from the variable, the original value is restored

## The code (5.1.22)

`config/harperConfigEnvVars.ts:701` — `cleanupRemovedEnvVar()` puts all three sources in the restore branch:

```ts
if (sourceName === 'HARPER_DEFAULT_CONFIG' || sourceName === 'HARPER_CONFIG' || sourceName === 'HARPER_SET_CONFIG') {
const pathsToCleanup = Object.keys(state.sources).filter((path) => state.sources[path] === sourceName);
for (const path of pathsToCleanup) {
if (path in state.originalValues) {
// Restore original value
setNestedValue(fileConfig, path, state.originalValues[path]);
```

Same in the shipped build, `dist/config/harperConfigEnvVars.js:592` (restore at `:597`).

And `processEnvVar()` deliberately captures originals for it — `config/harperConfigEnvVars.ts:620`:

```ts
if (sourceName === 'HARPER_SET_CONFIG') {
// SET_CONFIG always overrides everything, but store originals for restoration
applyConfigLayer(fileConfig, state, parsedConfig, sourceName, { respectSources: [], storeOriginals: true });
```

So the code comment agrees with the code and disagrees with the docs.

## Reproduction

Harper only — no other packages, no application code.

```bash
mkdir -p /tmp/hdb-repro && cd /tmp/hdb-repro
npm init -y >/dev/null
npm install @harperfast/harper@5.1.22

ROOT=/tmp/hdb-repro/data
BIN=/tmp/hdb-repro/node_modules/@harperfast/harper/dist/bin/harper.js

# Boot 1 — HARPER_SET_CONFIG forces a host-qualified ops port.
# The individual env var names the same key with a bare number.
ROOTPATH=$ROOT DEFAULTS_MODE=dev THREADS_COUNT=1 \
HDB_ADMIN_USERNAME=admin HDB_ADMIN_PASSWORD=harper-repro-pw \
HTTP_PORT=31926 LOCAL_STUDIO=false \
OPERATIONSAPI_NETWORK_PORT=31925 \
HARPER_SET_CONFIG="{\"rootPath\":\"$ROOT\",\"operationsApi\":{\"network\":{\"port\":\"127.0.0.1:31925\"}}}" \
node "$BIN" run .
# ...let it come up, then stop it.

# Boot 2 — same data dir, HARPER_SET_CONFIG absent.
ROOTPATH=$ROOT DEFAULTS_MODE=dev THREADS_COUNT=1 \
HDB_ADMIN_USERNAME=admin HDB_ADMIN_PASSWORD=harper-repro-pw \
HTTP_PORT=31926 LOCAL_STUDIO=false \
node "$BIN" run .
```

Inspecting `harper-config.yaml` and `/backup/.harper-config-state.json` after each boot:

```
=== BOOT 1: with HARPER_SET_CONFIG ===
harper-config.yaml operationsApi.network.port = "127.0.0.1:31925" (key PRESENT)
state.originalValues['operationsApi.network.port'] = 31925
state.sources['operationsApi.network.port'] = "HARPER_SET_CONFIG"
actual ops listener: 127.0.0.1:31925

=== BOOT 2: HARPER_SET_CONFIG absent ===
harper-config.yaml operationsApi.network.port = 31925 (key PRESENT)
state.originalValues['operationsApi.network.port'] = undefined
state.sources['operationsApi.network.port'] = undefined
actual ops listener: *:31925
```

**The key is still PRESENT after boot 2, holding the pre-`HARPER_SET_CONFIG` value.** That is the discriminator: had it been *deleted* as documented, `operationsApi.network.port` would be absent from `harper-config.yaml` and Harper's own default ops port would apply. Instead the exact prior value (`31925`) came back, and `state.originalValues` was consumed — that is a restore.

The listener line shows why it is not cosmetic: `operationsApi.network.port` accepts either a bare number (all interfaces) or `"host:port"` (explicit bind host), so restoring a bare original silently re-widens the ops API from loopback to all interfaces, and persists that into the config file for every subsequent boot.

## Precision note

The documented sentence describes removing **a key from** the variable, whereas the repro removes **the whole variable**. Both route through the same `cleanupRemovedEnvVar(fileConfig, state, 'HARPER_SET_CONFIG', 'HARPER_SET_CONFIG')` call (`config/harperConfigEnvVars.ts:810`), so the documented claim does not hold for the narrower case either — but I have only directly exercised whole-variable removal.

## Why this matters to a consumer

A consumer that reads the documented sentence will reason: *"`HARPER_SET_CONFIG` deletes on removal, so a stale value can't come back — it's safe to also set the individual env var for the same key."* That reasoning is what the docs license, and it is unsound against the shipped behaviour: the individual env var's value gets captured as the restore-to original, and reappears on the first boot that omits `HARPER_SET_CONFIG`.

We hit exactly this in Flair (): an ops API correctly bound to loopback at install silently re-bound to all interfaces on the first restart that spawned without `HARPER_SET_CONFIG`, and stayed that way. Our fix was to make every spawn carry the host-qualified value so the captured original is never a bare one — but that is a workaround for behaviour the docs say does not happen.

## Suggested resolution

Whichever way it should go:

- **If the code is correct**, the docs page needs the `HARPER_SET_CONFIG` bullet corrected — it currently reads as an explicit contrast against the other two variables, so it is likely to be relied on.
- **If the docs are correct**, `cleanupRemovedEnvVar` should not include `'HARPER_SET_CONFIG'` in the restore branch, and `processEnvVar` should not pass `storeOriginals: true` for it.

Happy to send a PR for either once you say which is intended.

Versions: `@harperfast/harper@5.1.22`, Node 26, macOS (the behaviour is config-layer only and not platform-specific; we first observed it on Linux).

Contributor guide

Open the contributing guide

Research direction

Start with config/harperConfigEnvVars.ts around processEnvVar(), cleanupRemovedEnvVar(), and the cited state handling; compare the behavior with the HARPER_SET_CONFIG section of the configuration documentation. Run the two-boot reproduction against @harperfast/harper@5.1.22 and confirm the persisted configuration and listener binding. Done means the intended removal semantics are decided and the code, documentation, and verification evidence agree.

Written by the indexing model from the issue text.

Assessment

Tech stack
nodejs, typescript
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.