plandex-ai/plandex · error

error reading JSON file: %v

Error message

error reading JSON file: %v

What it means

After the hash check, ModelSettingsCheckLocalChanges reads the model settings JSON file itself. Unlike the hash file, a missing settings file is NOT tolerated here — any os.ReadFile error is wrapped with this message, because the file was already confirmed to exist by the earlier fs.FileExists check.

Source

Thrown at app/cli/lib/model_settings.go:51

	hashPath := path + ".hash"

	exists, err := fs.FileExists(path)
	if err != nil {
		return ModelSettingsCheckLocalChangesResult{}, fmt.Errorf("error checking model settings file: %v", err)
	}

	if !exists {
		return ModelSettingsCheckLocalChangesResult{}, nil
	}

	lastSavedHash, err := os.ReadFile(hashPath)
	if err != nil && !os.IsNotExist(err) {
		return ModelSettingsCheckLocalChangesResult{}, fmt.Errorf("error reading hash file: %v", err)
	}

	localJsonData, err := os.ReadFile(path)
	if err != nil {
		return ModelSettingsCheckLocalChangesResult{}, fmt.Errorf("error reading JSON file: %v", err)
	}

	var clientModelPackSchemaRoles *shared.ClientModelPackSchemaRoles
	err = json.Unmarshal(localJsonData, &clientModelPackSchemaRoles)
	if err != nil {
		return ModelSettingsCheckLocalChangesResult{}, fmt.Errorf("error unmarshalling JSON file: %v", err)
	}

	currentHash, err := clientModelPackSchemaRoles.ToModelPackSchemaRoles().Hash()
	if err != nil {
		return ModelSettingsCheckLocalChangesResult{}, fmt.Errorf("error hashing model pack: %v", err)
	}

	modelPackSchemaRoles := clientModelPackSchemaRoles.ToModelPackSchemaRoles()

	return ModelSettingsCheckLocalChangesResult{
		HasLocalChanges:           currentHash != string(lastSavedHash),
		LocalModelPackSchemaRoles: &modelPackSchemaRoles,

View on GitHub (pinned to e2d772072e)

Solutions

  1. Re-run the check — if it was a delete race, the file will either exist again or be reported as absent.
  2. Check file permissions (chmod u+r) on the settings file.
  3. Avoid running file-sync tools that delete/replace the settings file while the CLI is running.
  4. If the path is a symlink, verify the target is readable.

Example fix

// before: blind read
// after: caller-side existence check to shrink the race window
if _, err := os.Stat(settingsPath); err != nil {
    return nil
}
res, err := ModelSettingsCheckLocalChanges(settingsPath)
Defensive patterns

Strategy: retry

Validate before calling

if _, err := os.Stat(settingsPath); err != nil {
    return nil
}
if f, err := os.Open(settingsPath); err != nil {
    return fmt.Errorf("settings file unreadable: %w", err)
} else {
    f.Close()
}

Try / catch

res, err := ModelSettingsCheckLocalChanges(settingsPath)
if err != nil {
    if strings.Contains(err.Error(), "reading JSON file") {
        time.Sleep(100 * time.Millisecond)
        res, err = ModelSettingsCheckLocalChanges(settingsPath)
    }
    if err != nil {
        return err
    }
}

Prevention

When it happens

Trigger: os.ReadFile(path) fails after fs.FileExists(path) returned true — a race where the file was deleted between the stat and the read, permission denied, or I/O error.

Common situations: Another process (editor, sync tool, git checkout) deletes/replaces the settings file concurrently; the file exists but was chmod'd unreadable; symlink to an unreadable target.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/64f10be84c6ba056. Report an issue: GitHub.