Skip to content

fix(coding-agents): don't let one stray file abort the Claude history import - #3778

Open
feniix wants to merge 2 commits into
vectorize-io:mainfrom
feniix:fix/claude-history-stray-file
Open

fix(coding-agents): don't let one stray file abort the Claude history import#3778
feniix wants to merge 2 commits into
vectorize-io:mainfrom
feniix:fix/claude-history-stray-file

Conversation

@feniix

@feniix feniix commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • claudeHistory prefiltered ~/.claude/projects candidates by encoded NAME and handed each match to jsonlFiles, which did existsSync — true for a regular file — and then readdirSync, which throws ENOTDIR. That escaped importLocalHistory, which documents that it never throws, and whose caller (installer.ts importConversations, line 1241) has no catch of its own: one junk file killed the whole --import-conversations run instead of costing that entry.
  • The guard belongs in the listing, not at the call site the issue suggested: jsonlFiles already owned "there is nothing here to list" via existsSync, and a stray file is that same case reached through a different errno. One listDir helper now holds it for the module.
  • That also closes the identical hole one level up, which the call-site fix would have left open — a regular file at ~/.claude/projects or ~/.dsh/sessions was equally fatal — and lets dshHistory drop its own copy of the try/catch.
  • Side effect: dropping the existsSync precheck sheds a stat per candidate directory (~100 on a real machine, per the claudeHistory docstring) and the TOCTOU window between check and read.

Closes #3771.

Type of change

  • Feature
  • Bug fix
  • Hotfix
  • Spike / exploration
  • Documentation
  • Refactor

Test plan

  • src/core/history.test.ts — new Claude regression test seeds one real session plus a stray file named like the project dir for a subdirectory. Verified it fails with the exact reported error when the guard is reverted (Error: ENOTDIR: not a directory, scandir '…/.claude/projects/-Users-x-dev-myrepo-sub') and passes with it.
  • Second test covers the dsh reader, whose per-project loop this refactors and which had no importLocalHistory coverage at all. It stubs $DSH_HOME so it can't read a developer's real sessions. Not a regression test — it passes either way.
  • npx vitest run src/core src/installer.test.ts → 579 passed. ./scripts/hooks/lint.sh clean.

Notes for the reviewer

  • feat/pi-harness has a call-site try/catch in piHistory (fde3bb3) that this makes redundant — it should collapse to listDir when the branches meet, and primeAgentHistory gets the fix for free.
  • Left alone as behaviour changes rather than this fix: codexHistory's walk is guarded at the opposite altitude (one unstattable entry discards every file found so far and reports "no past sessions found on disk"), importLocalHistory still has no top-level backstop for its never-throws contract, and installer.ts:1258-1259 creates its tmpfile outside the try whose comment says a failed backfill must not fail the install.

🤖 Generated with Claude Code

… import

claudeHistory prefilters project directories by encoded NAME and handed each
match to jsonlFiles, which did existsSync — true for a regular FILE — and then
readdirSync, which throws ENOTDIR. That escaped importLocalHistory, which
documents that it never throws, and whose caller (installer.ts
importConversations) has no catch: one junk file in ~/.claude/projects killed
the whole --import-conversations run rather than costing that entry. Fixes vectorize-io#3771.

The guard belongs in the listing itself, not at the call site: jsonlFiles already
owned "there is nothing here to list" via existsSync, and a stray file is that
same case reached through a different errno. One listDir helper now holds it for
the module, which also closes the identical hole one level up — a regular file at
~/.claude/projects or ~/.dsh/sessions was equally fatal — and lets dshHistory
drop its own copy of the try/catch. Dropping the existsSync precheck also sheds a
stat per candidate directory (~100 on a real machine) and its TOCTOU window.

The Claude regression test fails with the reported ENOTDIR without the guard; the
dsh test covers the reader whose loop this refactors, which had no coverage
through importLocalHistory at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@strix-security

strix-security Bot commented Aug 24, 2026

Copy link
Copy Markdown

Strix Security Review

Warning

This pull request has 17 commits after the last Strix review (364baf7). Strix has not reviewed these changes.
Automatic review on push is off for this repository. To review the latest changes, tag @strix-security in a comment, or turn on re-review on push.

No security issues found.

Updated for 364baf7.


Reviewed by Strix
Re-run review · Configure security review settings

…stray-file

# Conflicts:
#	hindsight-integrations/coding-agents/src/core/history.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant