plandex-ai/plandex · error

error reading JSON file: %v

Error message

error reading JSON file: %v

What it means

CustomModelsCheckLocalChanges reads the local custom-models JSON file after confirming it exists; a read failure (not absence — absence returns an empty result) is wrapped as 'error reading JSON file: %v'.

Source

Thrown at app/cli/lib/custom_models.go:107

	return serverModelsInput, nil
}

func CustomModelsCheckLocalChanges(path string) (CustomModelsCheckLocalChangesResult, error) {
	hashPath := path + ".hash"

	exists, err := fs.FileExists(path)
	if err != nil {
		return CustomModelsCheckLocalChangesResult{}, err
	}

	if !exists {
		return CustomModelsCheckLocalChangesResult{}, nil
	}

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

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

	localModelsInput := localClientModelsInput.ToModelsInput()

	lastSavedHash, err := os.ReadFile(hashPath)

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

	currentHash, err := localModelsInput.Hash()
	if err != nil {

View on GitHub (pinned to e2d772072e)

Solutions

  1. Check the wrapped OS error (%v) for the exact read failure
  2. Verify the file's permissions and that it is a regular file
  3. Delete the stale file and let the CLI regenerate it (SyncCustomModels rewrites it)
  4. Rerun the sync flow to recreate custom-models.json from server state

Example fix

// before
localJsonData, err := os.ReadFile(path)
if err != nil {
    return CustomModelsCheckLocalChangesResult{}, fmt.Errorf("error reading JSON file: %v", err)
}
// after: caller preflight
if info, err := os.Stat(path); err != nil || info.IsDir() {
    return CustomModelsCheckLocalChangesResult{}, nil // treat as missing, resync
}
Defensive patterns

Strategy: validation

Validate before calling

if info, err := os.Stat(path); err != nil || info.IsDir() {
    return CustomModelsCheckLocalChangesResult{}, nil // treat as absent, resync
}

Type guard

func fileReadableRegular(path string) bool {
    info, err := os.Stat(path)
    return err == nil && info.Mode().IsRegular()
}

Try / catch

res, err := CustomModelsCheckLocalChanges(...)
if err != nil && strings.Contains(err.Error(), "error reading JSON file") {
    os.Remove(path) // unreadable stale file; let sync recreate it
    return SyncCustomModels()
}

Prevention

When it happens

Trigger: os.ReadFile(path) fails on the local custom-models JSON that os.Stat already reported as existing — race where the file was deleted between stat and read, permission denied, or the path is a directory.

Common situations: Permissions changed on the models JSON between checks; another process (editor/sync tool) deleted or locked the file concurrently; file became a directory; disk I/O error.

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/14f6f66d9e2c8b7a. Report an issue: GitHub.