Skip to content

Commit 1576fb5

Browse files
committed
fix(config): persist config set under an env token; fail on ephemeral overrides (1.1.121)
A Socket API token supplied via env (SOCKET_CLI_API_TOKEN / SOCKET_SECURITY_API_TOKEN and legacy aliases) used to put the entire config into read-only mode, so `socket config set <key> <value>` silently failed to save while still printing `OK`, and a later `socket config get` then showed nothing. A token from the environment now overrides authentication only: unrelated keys such as defaultOrg are written to disk as expected, while the env token itself is still never persisted (getDefaultApiToken resolves it straight from the environment, so it is no longer mirrored into the cached config). When the config is genuinely ephemeral, because it was fully overridden via --config, SOCKET_CLI_CONFIG, or SOCKET_CLI_NO_API_TOKEN, `socket config set` now fails with a clear error instead of pretending it succeeded; the in-memory-only change is a no-op for a one-shot command. `config get apiToken` still reports the env-supplied token, which takes precedence over persisted / --config values. Adds unit and command-level regression tests and bumps the CLI to 1.1.121.
1 parent 80ccc51 commit 1576fb5

8 files changed

Lines changed: 134 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [1.1.121](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.121) - 2026-06-17
8+
9+
### Fixed
10+
- `socket config set` now persists correctly when a Socket API token is supplied via an environment variable. Previously, setting `SOCKET_CLI_API_TOKEN` / `SOCKET_SECURITY_API_TOKEN` put the entire config into read-only mode, so `socket config set <key> <value>` silently failed to save (and a later `socket config get` showed nothing) while still printing `OK`. A token from the environment now overrides authentication only: unrelated keys such as `defaultOrg` are written to disk as expected, and the env-supplied token itself is still never persisted.
11+
- `socket config set` no longer reports a misleading `OK` when the value genuinely cannot be saved. When the config is fully overridden (and therefore ephemeral) via `--config`, `SOCKET_CLI_CONFIG`, or `SOCKET_CLI_NO_API_TOKEN`, the command now fails with a clear error explaining that the value was not saved, instead of pretending it succeeded.
12+
713
## [1.1.120](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.120) - 2026-06-12
814

915
### Changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "socket",
3-
"version": "1.1.120",
3+
"version": "1.1.121",
44
"description": "CLI for Socket.dev",
55
"homepage": "https://github.com/SocketDev/socket-cli",
66
"license": "MIT",

src/commands/config/cmd-config-set.test.mts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
2+
import os from 'node:os'
13
import path from 'node:path'
24

35
import { describe, expect } from 'vitest'
@@ -114,4 +116,48 @@ describe('socket config get', async () => {
114116
expect(code, 'dry-run should exit with code 0 if input ok').toBe(0)
115117
},
116118
)
119+
120+
cmdit(
121+
['config', 'set', 'defaultOrg', 'my-test-org', FLAG_CONFIG, '{}'],
122+
'should fail (not report OK) when a full config override prevents persisting',
123+
async cmd => {
124+
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
125+
// A full --config override makes the config read-only, so the value cannot
126+
// be saved. `config set` is a no-op here, so it must fail rather than
127+
// report a misleading "OK".
128+
const combined = `${stdout}\n${stderr}`
129+
expect(combined).toContain('was not saved')
130+
expect(stdout).not.toContain('OK')
131+
expect(code, 'an unpersistable set should exit non-zero').toBe(1)
132+
},
133+
)
134+
135+
cmdit(
136+
['config', 'set', 'defaultOrg', 'my-test-org'],
137+
'should persist a non-token key when only the API token is overridden via env',
138+
async cmd => {
139+
const dataHome = mkdtempSync(path.join(os.tmpdir(), 'socket-cfg-'))
140+
try {
141+
const { code, stdout } = await spawnSocketCli(binCliPath, cmd, {
142+
env: {
143+
SOCKET_SECURITY_API_TOKEN: 'sktsec_faketoken',
144+
XDG_DATA_HOME: dataHome,
145+
},
146+
})
147+
expect(code, 'a persistable set should exit 0').toBe(0)
148+
expect(stdout).toContain('OK')
149+
150+
const raw = readFileSync(
151+
path.join(dataHome, 'socket', 'settings', 'config.json'),
152+
'utf8',
153+
)
154+
const saved = JSON.parse(Buffer.from(raw, 'base64').toString('utf8'))
155+
expect(saved.defaultOrg).toBe('my-test-org')
156+
// The env token must never be written to disk.
157+
expect(saved.apiToken).toBeUndefined()
158+
} finally {
159+
rmSync(dataHome, { recursive: true, force: true })
160+
}
161+
},
162+
)
117163
})
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { outputConfigGet } from './output-config-get.mts'
2+
import constants, { CONFIG_KEY_API_TOKEN } from '../../constants.mts'
23
import { getConfigValue } from '../../utils/config.mts'
34

4-
import type { OutputKind } from '../../types.mts'
5+
import type { CResult, OutputKind } from '../../types.mts'
56
import type { LocalConfig } from '../../utils/config.mts'
67

78
export async function handleConfigGet({
@@ -11,7 +12,19 @@ export async function handleConfigGet({
1112
key: keyof LocalConfig
1213
outputKind: OutputKind
1314
}) {
14-
const result = getConfigValue(key)
15+
// A Socket API token supplied via the environment (SOCKET_CLI_API_TOKEN /
16+
// SOCKET_SECURITY_API_TOKEN and legacy aliases, all aggregated into
17+
// constants.ENV.SOCKET_CLI_API_TOKEN) takes precedence over any persisted or
18+
// --config value. The env token is no longer mirrored into the in-memory
19+
// config (so unrelated keys stay persistable via `config set`), so surface it
20+
// explicitly here to preserve "env token wins" for `config get apiToken`.
21+
const { ENV } = constants
22+
const result: CResult<LocalConfig[keyof LocalConfig]> =
23+
key === CONFIG_KEY_API_TOKEN &&
24+
!ENV.SOCKET_CLI_NO_API_TOKEN &&
25+
ENV.SOCKET_CLI_API_TOKEN
26+
? { ok: true, data: ENV.SOCKET_CLI_API_TOKEN }
27+
: getConfigValue(key)
1528

1629
await outputConfigGet(key, result, outputKind)
1730
}

src/commands/config/handle-config-set.mts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug'
33
import { outputConfigSet } from './output-config-set.mts'
44
import { updateConfigValue } from '../../utils/config.mts'
55

6-
import type { OutputKind } from '../../types.mts'
6+
import type { CResult, OutputKind } from '../../types.mts'
77
import type { LocalConfig } from '../../utils/config.mts'
88

99
export async function handleConfigSet({
@@ -20,8 +20,23 @@ export async function handleConfigSet({
2020

2121
const result = updateConfigValue(key, value)
2222

23-
debugFn('notice', `Config update ${result.ok ? 'succeeded' : 'failed'}`)
24-
debugDir('inspect', { result })
23+
// `config set` is a one-shot command: an in-memory-only change is a no-op
24+
// because the process exits before anything reads it. updateConfigValue only
25+
// populates `data` when the config is read-only (a full --config /
26+
// SOCKET_CLI_CONFIG / SOCKET_CLI_NO_API_TOKEN override), so in that case
27+
// report a failure instead of a misleading success.
28+
const outcome: CResult<undefined | string> =
29+
result.ok && result.data
30+
? {
31+
ok: false,
32+
code: 1,
33+
message: `Config key '${key}' was not saved`,
34+
cause: result.data,
35+
}
36+
: result
2537

26-
await outputConfigSet(result, outputKind)
38+
debugFn('notice', `Config update ${outcome.ok ? 'succeeded' : 'failed'}`)
39+
debugDir('inspect', { result: outcome })
40+
41+
await outputConfigSet(outcome, outputKind)
2742
}

src/commands/config/output-config-set.mts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,9 @@ export async function outputConfigSet(
2626
logger.log(`# Update config`)
2727
logger.log('')
2828
logger.log(result.message)
29-
if (result.data) {
30-
logger.log('')
31-
logger.log(result.data)
32-
}
33-
} else {
34-
logger.log(`OK`)
35-
logger.log(result.message)
36-
if (result.data) {
37-
logger.log('')
38-
logger.log(result.data)
39-
}
29+
return
4030
}
31+
32+
logger.log(`OK`)
33+
logger.log(result.message)
4134
}

src/utils/config.mts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,19 @@ export function overrideCachedConfig(jsonConfig: unknown): CResult<undefined> {
304304

305305
export function overrideConfigApiToken(apiToken: unknown) {
306306
debugFn('notice', 'override: Socket API token (not stored)')
307-
// Set token to the local cached config and mark it read-only so it doesn't persist.
308-
_cachedConfig = {
309-
...config,
310-
...(apiToken === undefined ? {} : { apiToken: String(apiToken) }),
311-
} as LocalConfig
312-
_configFromFlag = true
307+
if (apiToken === undefined) {
308+
// SOCKET_CLI_NO_API_TOKEN: operate with no token and lock the config to
309+
// read-only so nothing is persisted for this run.
310+
_cachedConfig = { ...config } as LocalConfig
311+
_configFromFlag = true
312+
return
313+
}
314+
// A token supplied via env (SOCKET_CLI_API_TOKEN / SOCKET_SECURITY_API_TOKEN)
315+
// overrides authentication only. getDefaultApiToken() reads it straight from
316+
// the environment, so we intentionally do NOT inject it into the cached config
317+
// and do NOT mark the config read-only: unrelated keys (e.g. defaultOrg) can
318+
// still be saved with `socket config set`, while the env token never reaches
319+
// disk because it never enters the persisted cache.
313320
}
314321

315322
let _pendingSave = false
@@ -344,7 +351,10 @@ export function updateConfigValue<Key extends keyof LocalConfig>(
344351
return {
345352
ok: true,
346353
message: `Config key '${key}' was ${wasDeleted ? 'deleted' : `updated`}`,
347-
data: 'Change applied but not persisted; current config is overridden through env var or flag',
354+
data:
355+
'The active config is read-only because it was fully overridden by the ' +
356+
'--config flag, SOCKET_CLI_CONFIG, or SOCKET_CLI_NO_API_TOKEN. Remove ' +
357+
'the override to save changes to disk.',
348358
}
349359
}
350360

src/utils/config.test.mts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import { beforeEach, describe, expect, it } from 'vitest'
1212

1313
import {
1414
findSocketYmlSync,
15+
isConfigFromFlag,
1516
overrideCachedConfig,
17+
overrideConfigApiToken,
18+
resetConfigForTesting,
1619
updateConfigValue,
1720
} from './config.mts'
1821
import { testPath } from '../../test/utils.mts'
@@ -30,7 +33,7 @@ describe('utils/config', () => {
3033
updateConfigValue('defaultOrg', 'fake_test_org'),
3134
).toMatchInlineSnapshot(`
3235
{
33-
"data": "Change applied but not persisted; current config is overridden through env var or flag",
36+
"data": "The active config is read-only because it was fully overridden by the --config flag, SOCKET_CLI_CONFIG, or SOCKET_CLI_NO_API_TOKEN. Remove the override to save changes to disk.",
3437
"message": "Config key 'defaultOrg' was updated",
3538
"ok": true,
3639
}
@@ -54,6 +57,28 @@ describe('utils/config', () => {
5457
})
5558
})
5659

60+
describe('read-only state', () => {
61+
it('does not mark the config read-only when only the API token is overridden via env', () => {
62+
// A token from SOCKET_CLI_API_TOKEN / SOCKET_SECURITY_API_TOKEN overrides
63+
// auth only; unrelated keys must still be persistable.
64+
resetConfigForTesting()
65+
overrideConfigApiToken('sktsec_faketoken')
66+
expect(isConfigFromFlag()).toBe(false)
67+
})
68+
69+
it('marks the config read-only when fully overridden via --config / SOCKET_CLI_CONFIG', () => {
70+
resetConfigForTesting()
71+
overrideCachedConfig({})
72+
expect(isConfigFromFlag()).toBe(true)
73+
})
74+
75+
it('marks the config read-only when no token is forced (SOCKET_CLI_NO_API_TOKEN)', () => {
76+
resetConfigForTesting()
77+
overrideConfigApiToken(undefined)
78+
expect(isConfigFromFlag()).toBe(true)
79+
})
80+
})
81+
5782
describe('findSocketYmlSync', () => {
5883
it('should find socket.yml when walking up directory tree', () => {
5984
// This test verifies that findSocketYmlSync correctly walks up the directory

0 commit comments

Comments
 (0)