security: sanitize env vars from agent Bash subprocesses (#171)
Use a PreToolUse SDK hook to prepend `unset ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN` to every Bash command Kit runs, preventing secret leakage via env/printenv/echo/$PROC. Secrets are now passed via stdin JSON instead of mounted env files, closing all known exfiltration vectors. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ export interface ContainerInput {
|
||||
chatJid: string;
|
||||
isMain: boolean;
|
||||
isScheduledTask?: boolean;
|
||||
secrets?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ContainerOutput {
|
||||
@@ -157,33 +158,6 @@ function buildVolumeMounts(
|
||||
readonly: false,
|
||||
});
|
||||
|
||||
// Environment file directory (workaround for Apple Container -i env var bug)
|
||||
// Only expose specific auth variables needed by Claude Code, not the entire .env
|
||||
const envDir = path.join(DATA_DIR, 'env');
|
||||
fs.mkdirSync(envDir, { recursive: true });
|
||||
const envFile = path.join(projectRoot, '.env');
|
||||
if (fs.existsSync(envFile)) {
|
||||
const envContent = fs.readFileSync(envFile, 'utf-8');
|
||||
const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'];
|
||||
const filteredLines = envContent.split('\n').filter((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) return false;
|
||||
return allowedVars.some((v) => trimmed.startsWith(`${v}=`));
|
||||
});
|
||||
|
||||
if (filteredLines.length > 0) {
|
||||
fs.writeFileSync(
|
||||
path.join(envDir, 'env'),
|
||||
filteredLines.join('\n') + '\n',
|
||||
);
|
||||
mounts.push({
|
||||
hostPath: envDir,
|
||||
containerPath: '/workspace/env-dir',
|
||||
readonly: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mount agent-runner source from host — recompiled on container startup.
|
||||
// Bypasses Apple Container's sticky build cache for code changes.
|
||||
const agentRunnerSrc = path.join(projectRoot, 'container', 'agent-runner', 'src');
|
||||
@@ -206,6 +180,38 @@ function buildVolumeMounts(
|
||||
return mounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read allowed secrets from .env for passing to the container via stdin.
|
||||
* Secrets are never written to disk or mounted as files.
|
||||
*/
|
||||
function readSecrets(): Record<string, string> {
|
||||
const envFile = path.join(process.cwd(), '.env');
|
||||
if (!fs.existsSync(envFile)) return {};
|
||||
|
||||
const allowedVars = ['CLAUDE_CODE_OAUTH_TOKEN', 'ANTHROPIC_API_KEY'];
|
||||
const secrets: Record<string, string> = {};
|
||||
const content = fs.readFileSync(envFile, 'utf-8');
|
||||
|
||||
for (const line of content.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eqIdx = trimmed.indexOf('=');
|
||||
if (eqIdx === -1) continue;
|
||||
const key = trimmed.slice(0, eqIdx).trim();
|
||||
if (!allowedVars.includes(key)) continue;
|
||||
let value = trimmed.slice(eqIdx + 1).trim();
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
if (value) secrets[key] = value;
|
||||
}
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
function buildContainerArgs(mounts: VolumeMount[], containerName: string): string[] {
|
||||
const args: string[] = ['run', '-i', '--rm', '--name', containerName];
|
||||
|
||||
@@ -280,9 +286,12 @@ export async function runContainerAgent(
|
||||
let stdoutTruncated = false;
|
||||
let stderrTruncated = false;
|
||||
|
||||
// Write input and close stdin (Apple Container doesn't flush pipe without EOF)
|
||||
// Pass secrets via stdin (never written to disk or mounted as files)
|
||||
input.secrets = readSecrets();
|
||||
container.stdin.write(JSON.stringify(input));
|
||||
container.stdin.end();
|
||||
// Remove secrets from input so they don't appear in logs
|
||||
delete input.secrets;
|
||||
|
||||
// Streaming output: parse OUTPUT_START/END marker pairs as they arrive
|
||||
let parseBuffer = '';
|
||||
|
||||
Reference in New Issue
Block a user