Files
Codeman/test/env-overrides-schema.test.ts
T
Codeman maintainer 23d91a6ee1 feat(sessions): allow per-session CLAUDE_CONFIG_DIR env override (#255)
Adds an exact-key tier (ALLOWED_ENV_KEYS) beside ALLOWED_ENV_PREFIXES in
schemas.ts, admitting CLAUDE_CONFIG_DIR so a case can run on a separate
Claude subscription (client-billed accounts). Exact match only: other
CLAUDE_* keys and near-misses like CLAUDE_CONFIG_DIR_EXTRA stay rejected,
blocked keys stay blocked. The key also survives getEnvOverridesForPersist()
(a path, not a secret; dropping it would silently switch a rebuilt session
back to the default account after a reboot).

Docs cover the transcript caveat: a relocated config dir writes transcripts
outside ~/.claude/projects, so response viewer / subagent windows /
ultracode / Read My Mind go blind for that session unless projects is
symlinked back into the shared tree.

Design and spec contributed by @jordan8037310 in #255. Closes #255.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 22:39:07 +02:00

80 lines
2.7 KiB
TypeScript

/**
* @fileoverview envOverrides allowlist: exact-key entries alongside the prefixes.
*
* CLAUDE_CONFIG_DIR (#255) relocates the Claude CLI's user config (credentials,
* settings, stats) so a case can run on a separate Claude subscription. It starts
* with `CLAUDE_`, not `CLAUDE_CODE_`, so the prefix allowlist alone rejects it;
* ALLOWED_ENV_KEYS in schemas.ts admits it as an exact match. These tests pin:
* the exact key is accepted, near-misses stay rejected (no accidental prefix
* widening), blocked keys stay blocked, and the key survives persist filtering
* (losing it on restart would silently move a session back to the default account).
*/
import { describe, it, expect } from 'vitest';
import { CreateSessionSchema } from '../src/web/schemas.js';
import { Session } from '../src/session.js';
describe('envOverrides exact-key allowlist', () => {
it('accepts CLAUDE_CONFIG_DIR', () => {
const parsed = CreateSessionSchema.parse({
workingDir: '/tmp',
mode: 'claude',
envOverrides: { CLAUDE_CONFIG_DIR: '/home/user/.claude-clients/acme' },
});
expect(parsed.envOverrides).toEqual({ CLAUDE_CONFIG_DIR: '/home/user/.claude-clients/acme' });
});
it('accepts CLAUDE_CONFIG_DIR alongside prefix-allowlisted keys', () => {
const parsed = CreateSessionSchema.parse({
workingDir: '/tmp',
mode: 'claude',
envOverrides: {
CLAUDE_CONFIG_DIR: '/home/user/.claude-clients/acme',
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
},
});
expect(Object.keys(parsed.envOverrides ?? {})).toHaveLength(2);
});
it('rejects other CLAUDE_-prefixed keys (exact match only, no prefix widening)', () => {
expect(() =>
CreateSessionSchema.parse({
workingDir: '/tmp',
envOverrides: { CLAUDE_SOMETHING_ELSE: 'x' },
})
).toThrow();
expect(() =>
CreateSessionSchema.parse({
workingDir: '/tmp',
envOverrides: { CLAUDE_CONFIG_DIR_EXTRA: '/tmp/x' },
})
).toThrow();
});
it('still blocks security-sensitive keys', () => {
for (const key of ['PATH', 'LD_PRELOAD', 'NODE_OPTIONS', 'CODEMAN_MUX_NAME']) {
expect(() =>
CreateSessionSchema.parse({
workingDir: '/tmp',
envOverrides: { [key]: 'x' },
})
).toThrow();
}
});
});
describe('CLAUDE_CONFIG_DIR persistence', () => {
it('survives the state.json persist filter (path, not a secret)', () => {
const session = new Session({
workingDir: '/tmp',
envOverrides: {
CLAUDE_CONFIG_DIR: '/home/user/.claude-clients/acme',
OPENCODE_API_KEY: 'secret-must-not-persist',
},
});
expect(session.getEnvOverridesForPersist()).toEqual({
CLAUDE_CONFIG_DIR: '/home/user/.claude-clients/acme',
});
});
});