JuliusBrussee/caveman · error · Error
cave_memory_tenant_invalid
Error message
cave_memory_tenant_invalid
What it means
memoryFilePath() validates the tenant component of the durable memory path against a strict [a-z0-9_-]-class pattern (no dots or path separators) so no tenant value can traverse out of the memory root. The sentinel "_" is reserved for the default tenant. A tenant containing uppercase, slashes, "..", ".json", or other characters throws cave_memory_tenant_invalid.
Source
Thrown at packages/agent/src/memory-store.ts:59
const NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,95}$/;
function defaultRoot(): string {
return process.env.CAVE_AGENT_MEMORY_ROOT ?? join(homedir(), ".caveman", "agent-memory");
}
/**
* The durable file for (tenant, agentId, namespace). The three scoping
* components are validated to a `[a-z0-9_-]`-class charset with no `.` or path
* separator, so no component can traverse out of the memory root.
*/
export function memoryFilePath(
config: MemoryStoreConfig | undefined,
agentId: string,
namespace: string,
): string {
const tenant = config?.tenant ?? "_";
if (tenant !== "_" && !TENANT_PATTERN.test(tenant)) {
throw new Error("cave_memory_tenant_invalid");
}
if (!AGENT_PATTERN.test(agentId)) throw new Error("cave_memory_agent_invalid");
if (!NAMESPACE_PATTERN.test(namespace)) throw new Error("cave_memory_namespace_invalid");
return join(config?.root ?? defaultRoot(), tenant, agentId, `${namespace}.json`);
}
function isMemoryEntry(value: unknown): value is MemoryEntry {
return value !== null && typeof value === "object" &&
typeof (value as { text?: unknown }).text === "string" &&
Number.isSafeInteger((value as { createdAt?: unknown }).createdAt);
}
/** Read the durable entries. A missing or corrupt file is an empty store, never a throw into a run. */
export async function readMemories(filePath: string): Promise<MemoryEntry[]> {
try {
const parsed: unknown = JSON.parse(await readFile(filePath, "utf8"));
return Array.isArray(parsed) ? parsed.filter(isMemoryEntry) : [];
} catch {View on GitHub (pinned to 27d5a3981a)
Solutions
- Slugify the tenant before configuring the store: lowercase, replace everything outside [a-z0-9_-] with "-"
- Use the reserved "_" only for the shared/default tenant, never as a real tenant id
- Omit tenant in MemoryStoreConfig when you want the default
- Add a unit test asserting your tenant ids match /^[a-z0-9_-]+$/ at the API boundary
Example fix
// before
const store = memoryStore({ tenant: account.organizationName }); // "Acme Corp!"
// after
const slug = account.organizationName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "_";
const store = memoryStore({ tenant: slug }); Defensive patterns
Strategy: validation
Validate before calling
const TENANT_RE = /^[a-z0-9_-]+$/;
function tenantSlug(raw: string): string {
const slug = raw.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
if (slug === "" || !TENANT_RE.test(slug)) throw new Error(`cannot derive safe tenant id from ${JSON.stringify(raw)}`);
return slug;
} Type guard
const isSafeTenant = (t: string): boolean => t === "_" || /^[a-z0-9_-]+$/.test(t);
Prevention
- Slugify tenant ids at the API boundary, never store raw org/user names
- Reserve "_" for the default tenant in your data model
- Assert the pattern in an integration test that covers every tenant source
When it happens
Trigger: Setting config.tenant to values like "Acme Corp", "tenant/../etc", "my.tenant", or "" (empty string is not "_"). Only "_" and pattern-conforming strings pass; omitting tenant config entirely defaults to "_" and never throws.
Common situations: Using a raw hostname, email, or user display name as tenant id; forgetting to slugify tenant ids at system boundaries; or passing null/undefined wrapped as the string "undefined".
Related errors
- cave_memory_agent_invalid
- invalid Claude organization id
- cave_live_eval_sandbox_profile_escapes_root
- caveman-code: path escapes the workspace: ${candidate}
- caveman agent: file source escapes project root
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/92b1bac51d7eee72.
Report an issue: GitHub.