plandex-ai/plandex · error

error reading hash file: %v

Error message

error reading hash file: %v

What it means

ModelSettingsCheckLocalChanges reads <path>.hash to get the last-saved hash for comparison. A missing hash file is tolerated (os.IsNotExist is allowed); any other read error is wrapped with this message, meaning local-change detection cannot proceed.

Source

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

	HasLocalChanges           bool
	LocalModelPackSchemaRoles *shared.ModelPackSchemaRoles
}

func ModelSettingsCheckLocalChanges(path string) (ModelSettingsCheckLocalChangesResult, error) {
	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)
	}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Check permissions on the .hash file from the wrapped error path; chmod to make it readable.
  2. If a directory occupies the .hash path, remove it (the hash will be regenerated).
  3. As a last resort, delete the .hash file so the next sync treats the file as unhashed and recreates it.

Example fix

// before
// (no pre-check)
// after: ensure the hash sidecar is readable or absent before calling
if info, err := os.Stat(hashPath); err == nil && info.IsDir() {
    os.RemoveAll(hashPath)
}
Defensive patterns

Strategy: validation

Validate before calling

if info, err := os.Stat(settingsPath + ".hash"); err == nil && info.IsDir() {
    os.RemoveAll(settingsPath + ".hash")
}

Try / catch

if err != nil {
    if strings.Contains(err.Error(), "reading hash file") && !os.IsNotExist(errors.Unwrap(err)) {
        return fmt.Errorf("hash sidecar unreadable at %s.hash — fix perms or delete it to regenerate: %w", settingsPath, err)
    }
    return err
}

Prevention

When it happens

Trigger: os.ReadFile on <settingsPath>.hash fails with a non-NotNotExist error: permission denied, path is a directory, or I/O error on the hash sidecar file.

Common situations: The .hash file exists but is unreadable (owned by another user, or created with restrictive perms); a directory named <file>.hash was created accidentally; EDR/antivirus blocking access to dotfiles.

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