{"record":{"id":"a521a24a0d69ba52","repo":"can1357/oh-my-pi","slug":"secret-placeholder-key-at-keypath-exists-but-is","errorCode":null,"errorMessage":"secret placeholder key at ${keyPath} exists but is empty or unreadable","messagePattern":"secret placeholder key at (.+?) exists but is empty or unreadable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/secrets/index.ts","lineNumber":48,"sourceCode":"\t\tcachedPlaceholderKeys.set(keyPath, existing);\n\t\treturn existing;\n\t}\n\n\tconst generated = crypto.randomBytes(32).toString(\"base64url\");\n\tawait fs.promises.mkdir(path.dirname(keyPath), { recursive: true });\n\ttry {\n\t\tawait fs.promises.writeFile(keyPath, generated, { flag: \"wx\", mode: 0o600 });\n\t\tcachedPlaceholderKeys.set(keyPath, generated);\n\t\treturn generated;\n\t} catch (err) {\n\t\tif ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n\t\t// Another process won the create race but may still be mid-write: `wx`\n\t\t// creates the file empty before the bytes land. Wait for non-empty content\n\t\t// instead of caching an empty key (which would be a known, dictionaryable\n\t\t// key and would not match tokens other processes persist with the real key).\n\t\tconst winner = await readPlaceholderKeyFile(keyPath, true);\n\t\tif (winner === undefined) {\n\t\t\tthrow new Error(`secret placeholder key at ${keyPath} exists but is empty or unreadable`);\n\t\t}\n\t\tcachedPlaceholderKeys.set(keyPath, winner);\n\t\treturn winner;\n\t}\n}\n\n/** Return an existing placeholder key for redaction without creating a new key file. */\nexport async function getExistingSecretPlaceholderKey(keyDir?: string): Promise<string | undefined> {\n\tconst keyPath = keyDir ? path.join(keyDir, \"secret-placeholder.key\") : getSecretPlaceholderKeyPath();\n\tconst cached = cachedPlaceholderKeys.get(keyPath);\n\tif (cached !== undefined) return cached;\n\t// Redaction-only: this key is loaded solely to redact an existing key file from\n\t// provider-visible tool output, never to mint placeholders. A truncated/corrupt\n\t// or unreadable key must NOT block startup for replace-only/no-secret sessions —\n\t// an invalid key is not a usable HMAC anyway, and a tool reading the same file\n\t// gets the same bytes, so there is nothing sensitive to redact.\n\tlet existing: string | undefined;\n\ttry {","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/secrets/index.ts#L30-L66","documentation":"Secret placeholder keys are stored in a key file created with an exclusive 'wx' flag so only one process generates the key. If the file exists but readPlaceholderKeyFile cannot obtain non-empty valid content (another process is mid-write, or the file is corrupted/unreadable), the code refuses to cache or derive a key.","triggerScenarios":"Concurrent processes racing to create the placeholder key file; the key file exists with zero bytes or cannot be read/decoded.","commonSituations":"Crash between file creation and write leaving an empty file; permission problems on the secrets directory; heavy multi-process startup (multiple omp instances) hitting the create race.","solutions":["Delete the empty/corrupt key file so a healthy process can recreate it","Wait and retry — the winning process may still be mid-write","Check filesystem permissions on the secrets directory"],"exampleFix":"// before\n// keyPath exists but is 0 bytes\nthrow new Error(`secret placeholder key at ${keyPath} exists but is empty or unreadable`);\n// after\nawait fs.rm(keyPath); // remove the empty/corrupt file, then retry the operation","handlingStrategy":"retry","validationCode":"const stat = await fs.stat(keyPath).catch(() => null);\nif (stat && stat.size === 0) throw new Error(\"placeholder key file is empty; delete it and retry\");","typeGuard":"null","tryCatchPattern":"try {\n  key = await getSecretPlaceholderKey();\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"empty or unreadable\")) {\n    await Bun.sleep(50); // creator may still be mid-write\n    key = await getSecretPlaceholderKey();\n  } else throw err;\n}","preventionTips":["Avoid killing processes during first-run key creation","Check directory permissions for the secrets path","On persistent failure, delete the empty key file and let it regenerate"],"tags":["secrets","filesystem","concurrency"],"backgroundTag":"secret-key-unreadable","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}