Skip to content

coding-agents: --import-conversations dies on a stray file in ~/.claude/projects instead of skipping it #3771

Description

@feniix

Bug Description

importLocalHistory documents that it never throws (hindsight-integrations/coding-agents/src/core/history.ts), but the Claude reader does. A single regular file sitting where a project directory is expected under ~/.claude/projects aborts the entire --import-conversations run instead of costing that one entry.

claudeHistory prefilters candidate project directories by encoded name and hands each match to jsonlFiles, which does existsSync(dir) — true for a file — and then readdirSync(dir), which throws ENOTDIR. The caller, importConversations in installer.ts, has no try/catch of its own, so the throw escapes the install.

Steps to Reproduce

# a normal Claude session for the repo, plus one stray file named like a project dir
touch ~/.claude/projects/-Users-x-dev-myrepo-sub
cd /Users/x/dev/myrepo
npx @vectorize-io/hindsight-coding-agents install claude-code --import-conversations

Equivalent as a test, against importLocalHistory directly:

writeFileSync(claudeProjectDir("/Users/x/dev/myrepo/sub", home), "not a folder");
importLocalHistory("claude-code", "/Users/x/dev/myrepo", home);

Expected Behavior

The stray entry is skipped and the real sessions still import — the same way dshHistory already handles it ("a stray file where a project directory was expected") and codexHistory wraps its walk.

Actual Behavior

Error: ENOTDIR: not a directory, scandir '…/.claude/projects/-Users-x-dev-myrepo-sub', and nothing is imported.

Measured across all five file-based readers, each seeded with one real session and one stray entry:

reader no stray with stray
claude-code 1 session THREW ENOTDIR
codex 1 session 1 session
dsh 1 session 1 session
pi 1 session 1 session (fixed, see below)
prime-agent 1 session 1 session

Suggested Fix

Wrap the per-directory listing, exactly as the pi reader now does:

for (const dir of candidateDirs) {
  try {
    files.push(...jsonlFiles(join(root, dir)));
  } catch {
    // stray file where a project folder was expected — skip the entry, not the run
  }
}

Notes

piHistory was modelled on claudeHistory and inherited this, which is how it was found; the pi side is already fixed and carries a regression test that fails with ENOTDIR without the guard. claude-code was deliberately left out of that change to keep it scoped, hence this issue.

Likelihood is low — the stray file has to be named like an encoded project directory — but the failure mode is the whole import dying rather than degrading.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions