Write Tool Rejects Writing Plans To Default Plan Location ../../.claude/plans/

Resolved 💬 6 comments Opened Mar 21, 2026 by johnzfitch Closed Apr 20, 2026

Bug

When plan mode is active, the system prompt instructs the model to write its plan to ~/.claude/plans/<slug>.md. The model calls the Write tool with that absolute path. The harness computes path.relative(cwd, targetPath), which produces a ../../.claude/plans/… relative path. This relative path is then validated by the bundled ignore library, which rejects any path containing ../ because allowRelativePaths defaults to false.

The Write tool throws:

path should be a `path.relative()`d string, but got "../../.claude/plans/valiant-popping-sketch.md"

The binary&rsquo;s own plan-file code writes to the same ~/.claude/plans/ directory via direct fs.writeFileSync and succeeds &mdash; only the model&rsquo;s Write tool is affected.

Root Cause (pseudocode)

planDir = settings.plansDirectory
           ? resolve(projectRoot, settings.plansDirectory)
           : join(CLAUDE_CONFIG_DIR, "plans")   // ~/.claude/plans/

// Write tool path validation (simplified):
rel = path.relative(cwd, targetPath)          // "../../.claude/plans/slug.md"
ignoreInstance.test(rel)                       // <-- throws RangeError

The ignore instance is constructed with allowRelativePaths defaulting to false. Its isNotRelative check rejects any path starting with ../:

isNotRelative(path):
  return /^\.\.\//.test(path)    // true for ../../.claude/plans/...

_test(path):
  if strictPathCheck and isNotRelative(path):
    throw RangeError("path should be a path.relative()'d string")

Additional Finding: plansDirectory Workaround Is Also Broken

Setting plansDirectory to an absolute path like ~/.claude/plans does not work because the resolver enforces that the resolved path is within the project root:

resolved = path.resolve(projectRoot, plansDirectory)
if !resolved.startsWith(projectRoot):
    log(Error("plansDirectory must be within project root"))
    fallback to ~/.claude/plans/   // the broken default

The only accepted value is a relative path inside the project (e.g., ".claude/plans"). However, even with this set, the system prompt still tells the model to write to ~/.claude/plans/ &mdash; it does not respect the plansDirectory setting. So the workaround fails at two levels:

  1. Absolute paths outside project root are silently rejected (falls back to broken default)
  2. The system prompt ignores plansDirectory and always points to ~/.claude/plans/

Expected Behavior

The Write tool should accept the plan file path that the system prompt told the model to use. Either:

  1. Exempt plan file paths from the ../ check
  2. Pass allowRelativePaths: true to the ignore instance used for write validation
  3. Resolve the plan file to an absolute path before the relative-path check
  4. Have the system prompt respect plansDirectory when set

Reproduction

  1. cd /any/project (any directory that is not ~/.claude)
  2. Start Claude Code, enter plan mode
  3. Model attempts to write plan to ~/.claude/plans/<slug>.md
  4. Write tool throws RangeError
  5. Setting "plansDirectory": "/home/user/.claude/plans" silently falls back to the same broken path
  6. Setting "plansDirectory": ".claude/plans" is accepted by the resolver but the system prompt still points to ~/.claude/plans/

Version

Observed in 2.1.81. Plans directory resolution is identical from 2.1.75&ndash;2.1.81 (checked via binary analysis). The regression may have been introduced when the Write tool started routing all paths through the ignore library&rsquo;s validation.

View original on GitHub ↗

This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗