hasura/graphql-engine · warning

file %q is not a symlink (mode=%s)

Error message

file %q is not a symlink (mode=%s)

What it means

removeLink refuses to delete a path that is not a symbolic link (on non-Windows platforms) as a safety measure, so uninstalling a plugin never removes a regular file or directory the user placed in the bin directory. It reports the offending file's mode so you can see what it actually is.

Source

Thrown at cli/plugins/util.go:240

		}
	}

	return nil
}

// removeLink removes a symlink reference if exists.
func removeLink(path string) error {
	var op errors.Op = "plugins.removeLink"

	fi, err := os.Lstat(path)
	if stderrors.Is(err, fs.ErrNotExist) {
		return nil
	} else if err != nil {
		return errors.E(op, fmt.Errorf("failed to read the symlink in %q: %w", path, err))
	}

	if fi.Mode()&os.ModeSymlink == 0 && !IsWindows() {
		return errors.E(op, fmt.Errorf("file %q is not a symlink (mode=%s)", path, fi.Mode()))
	}

	if err := os.Remove(path); err != nil {
		return errors.E(op, fmt.Errorf("failed to remove the symlink in %q: %w", path, err))
	}

	return nil
}

// PluginNameToBin creates the name of the symlink file for the plugin name.
// It converts dashes to underscores.
func PluginNameToBin(name string, isWindows bool) string {
	name = strings.ReplaceAll(name, "-", "_")

	name = "hasura-" + name
	if isWindows {
		name += ".exe"
	}

View on GitHub (pinned to 724551b9ae)

Solutions

  1. Inspect the file: ls -l ~/.hasura/plugins/bin/<name> to confirm it is a regular file, not a link.
  2. If it is a manually installed copy, delete it yourself (rm, not the CLI) and re-run the CLI command.
  3. If it is a directory, remove it with rm -rf and reinstall the plugin so the CLI manages the symlink.
  4. Never force the CLI past this; the guard protects your data.

Example fix

# before
$ hasura plugin uninstall myplugin
file "/home/u/.hasura/plugins/bin/myplugin" is not a symlink (mode=-rwxr-xr-x)

# after
$ rm /home/u/.hasura/plugins/bin/myplugin
$ hasura plugin uninstall myplugin   # succeeds
Defensive patterns

Strategy: validation

Validate before calling

func isSymlinkOrMissing(path string) bool {
    fi, err := os.Lstat(path)
    if errors.Is(err, fs.ErrNotExist) { return true }
    return err == nil && fi.Mode()&os.ModeSymlink != 0
}
// call before Uninstall/createOrUpdateLink
if !isSymlinkOrMissing(dst) { /* move/delete the regular file yourself */ }

Type guard

func isSymlink(path string) bool {
    fi, err := os.Lstat(path)
    return err == nil && fi.Mode()&os.ModeSymlink != 0
}

Try / catch

// treat as a warning: the CLI is protecting a real file
if err := uninstall(); err != nil && strings.Contains(err.Error(), "is not a symlink") {
    // manually inspect/remove the file, then retry
}

Prevention

When it happens

Trigger: Calling Uninstall or createOrUpdateLink when the destination path exists as a regular file, directory, hardlink, or other non-symlink inode on Linux/macOS. On Windows this check is skipped (IsWindows guard).

Common situations: A user manually copied the plugin binary into the bin directory instead of installing via the CLI, or a previous installer version wrote a real file rather than a symlink; later uninstall then trips this guard.

Related errors


AI-assisted analysis of hasura/graphql-engine@724551b9ae (2026-08-28). Data as JSON: /api/errors/6e5bd89d770403f8. Report an issue: GitHub.