asdf-vm/asdf · error

unable to read last updated file: %w

Error message

unable to read last updated file: %w

What it means

lastUpdated reads the modification time of the index touch file to compute how stale the index is. If os.Stat on the touch file (repoUpdatedFilename) fails, the error is wrapped as 'unable to read last updated file'. Typically the touch file was never created because the index was never initialized.

Source

Thrown at internal/pluginindex/pluginindex.go:146

	return url, nil
}

func touchFS(directory string) (bool, error) {
	filename := filepath.Join(directory, repoUpdatedFilename)
	file, err := os.OpenFile(filename, os.O_RDONLY|os.O_CREATE, 0o666)
	if err != nil {
		return false, fmt.Errorf("unable to create file plugin index touch file: %w", err)
	}

	file.Close()
	return true, nil
}

func lastUpdated(dir string) (int64, error) {
	info, err := os.Stat(filepath.Join(dir, repoUpdatedFilename))
	if err != nil {
		return 0, fmt.Errorf("unable to read last updated file: %w", err)
	}

	// info.Atime_ns now contains the last access time
	updated := time.Now().UnixNano() - info.ModTime().UnixNano()
	return updated, nil
}

func readPlugin(dir, name string) (string, error) {
	filename := filepath.Join(dir, "plugins", name)

	pluginInfo, err := ini.Load(filename)
	if err != nil {
		return "", fmt.Errorf("plugin %s not found in repository", name)
	}

	return pluginInfo.Section("").Key("repository").String(), nil
}

View on GitHub (pinned to 074a1722ca)

Solutions

  1. Let Refresh recover: a missing touch file leads to re-initialization — clear any stale/corrupt index directory and rerun.
  2. Recreate the index from scratch: rm -rf the index/repository dir under ASDF_DATA_DIR, then run any plugin index command.
  3. Check permissions on the index directory so the touch file can be read.
  4. Ensure two asdf processes are not racing on the same data dir.

Example fix

// before: half-initialized index missing touch file
ls ~/.asdf/repository/plugin-index  # file absent
// after: reset the index
rm -rf "${ASDF_DATA_DIR:-$HOME/.asdf}/repository"
asdf plugin list all
Defensive patterns

Strategy: validation

Validate before calling

touch := filepath.Join(indexDir, "plugin-index-updated")
if _, err := os.Stat(touch); os.IsNotExist(err) {
    // index never initialized; force a fresh Refresh/clone
    os.RemoveAll(indexDir)
}

Try / catch

ok, err := idx.Refresh()
if err != nil && strings.Contains(err.Error(), "unable to read last updated file") {
    os.RemoveAll(indexDir)
    ok, err = idx.Refresh() // re-initialize from scratch
}

Prevention

When it happens

Trigger: Refresh calling lastUpdated when: the touch file does not exist yet (first run, partially completed init); the index directory was deleted or recreated without the touch file; permission denied on Stat; the directory path is wrong.

Common situations: A previous clone succeeded but touchFS failed or was interrupted; user manually cleaned the asdf repository directory; race between two asdf processes; data dir moved between asdf versions.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of asdf-vm/asdf@074a1722ca (2026-08-30). Data as JSON: /api/errors/e611eedd9e8fb7ec. Report an issue: GitHub.