thedotmack/claude-mem · error

Refusing inject write outside agent memory/log

Error message

Refusing inject write outside agent memory/log

What it means

assertSafeInjectPath confines all writes to the directory agentDataRoot/agents/<agentId>/memory/log. If the resolved path's parent directory differs from that expected directory, it throws, blocking writes anywhere else on the filesystem (traversal/symlink protection).

Solutions

  1. Always build the target path with injectLogPath(agentDataRoot, agentId) so it lands inside memory/log.
  2. Remove any '..' segments or user-supplied path components from the target before calling.
  3. Verify agentDataRoot matches the actual (realpath'd) root so expectedDir equals the resolved dirname.

Example fix

// before
const p = path.join(root, 'agents', agentId, 'memory', 'logs', 'inject.md');
// after
const p = injectLogPath(root, agentId); // .../memory/log/<INJECT_LOG_BASENAME>
Defensive patterns

Strategy: validation

Validate before calling

const expected = path.resolve(path.join(root, 'agents', agentId, 'memory', 'log'));
if (path.dirname(path.resolve(target)) !== expected) {
  throw new Error('target escapes agent memory/log');
}

Try / catch

try {
  assertSafeInjectPath(root, agentId, target);
} catch (err) {
  logger.error('Refusing unsafe inject path', { target }, err);
  return;
}

Prevention

When it happens

Trigger: refreshSeatIndex or ensureIndexLogDir passes a filePath whose path.resolve'd dirname is not exactly the agent's memory/log directory — e.g. paths containing '..', absolute paths to other folders, or symlinked locations that resolve elsewhere.

Common situations: Caller joins a user-supplied subpath; agent data root was moved or symlinked so realpath diverges; a typo passes 'logs' instead of 'log'.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/b6464899d657503d. Report an issue: GitHub.

Appendix: source

Thrown at src/services/integrations/grok-bot-index-format.ts:175

export function shouldRewriteInject(existingContents: string, nextContents: string): boolean {
  return factBlock(existingContents) !== factBlock(nextContents);
}

export function injectLogPath(agentDataRoot: string, agentId: string): string {
  if (!AGENT_ID_RE.test(agentId)) {
    throw new Error(`Refusing inject path for non-UUID agent id: ${agentId}`);
  }
  return path.join(agentDataRoot, 'agents', agentId, 'memory', 'log', INJECT_LOG_BASENAME);
}

export function assertSafeInjectPath(agentDataRoot: string, agentId: string, filePath: string): void {
  const expectedDir = path.resolve(path.join(agentDataRoot, 'agents', agentId, 'memory', 'log'));
  const resolved = path.resolve(filePath);
  if (path.basename(resolved).toLowerCase() === 'profile.md') {
    throw new Error('Refusing write to profile.md');
  }
  if (path.dirname(resolved) !== expectedDir) {
    throw new Error('Refusing inject write outside agent memory/log');
  }
  if (path.basename(resolved) !== INJECT_LOG_BASENAME) {
    throw new Error(`Refusing inject write to a file this writer does not own: ${path.basename(resolved)}`);
  }
}

function writeFileAtomic(filePath: string, contents: string): void {
  mkdirSync(path.dirname(filePath), { recursive: true });
  const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
  writeFileSync(tmp, contents, 'utf8');
  renameSync(tmp, filePath);
}

export function writeFileIfChanged(filePath: string, contents: string): { changed: boolean; filePath: string } {
  if (existsSync(filePath) && readFileSync(filePath, 'utf8') === contents) {
    return { changed: false, filePath };
  }
  writeFileAtomic(filePath, contents);

View on GitHub (pinned to d8bc9755e7)