{"record":{"id":"6e5bd89d770403f8","repo":"hasura/graphql-engine","slug":"file-q-is-not-a-symlink-mode-s","errorCode":null,"errorMessage":"file %q is not a symlink (mode=%s)","messagePattern":"file %q is not a symlink \\(mode=(.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"cli/plugins/util.go","lineNumber":240,"sourceCode":"\t\t}\n\t}\n\n\treturn nil\n}\n\n// removeLink removes a symlink reference if exists.\nfunc removeLink(path string) error {\n\tvar op errors.Op = \"plugins.removeLink\"\n\n\tfi, err := os.Lstat(path)\n\tif stderrors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t} else if err != nil {\n\t\treturn errors.E(op, fmt.Errorf(\"failed to read the symlink in %q: %w\", path, err))\n\t}\n\n\tif fi.Mode()&os.ModeSymlink == 0 && !IsWindows() {\n\t\treturn errors.E(op, fmt.Errorf(\"file %q is not a symlink (mode=%s)\", path, fi.Mode()))\n\t}\n\n\tif err := os.Remove(path); err != nil {\n\t\treturn errors.E(op, fmt.Errorf(\"failed to remove the symlink in %q: %w\", path, err))\n\t}\n\n\treturn nil\n}\n\n// PluginNameToBin creates the name of the symlink file for the plugin name.\n// It converts dashes to underscores.\nfunc PluginNameToBin(name string, isWindows bool) string {\n\tname = strings.ReplaceAll(name, \"-\", \"_\")\n\n\tname = \"hasura-\" + name\n\tif isWindows {\n\t\tname += \".exe\"\n\t}","sourceCodeStart":222,"sourceCodeEnd":258,"githubUrl":"https://github.com/hasura/graphql-engine/blob/724551b9ae87845594ef0408cff0e50eb6c90dc5/cli/plugins/util.go#L222-L258","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Inspect the file: ls -l ~/.hasura/plugins/bin/<name> to confirm it is a regular file, not a link.","If it is a manually installed copy, delete it yourself (rm, not the CLI) and re-run the CLI command.","If it is a directory, remove it with rm -rf and reinstall the plugin so the CLI manages the symlink.","Never force the CLI past this; the guard protects your data."],"exampleFix":"# before\n$ hasura plugin uninstall myplugin\nfile \"/home/u/.hasura/plugins/bin/myplugin\" is not a symlink (mode=-rwxr-xr-x)\n\n# after\n$ rm /home/u/.hasura/plugins/bin/myplugin\n$ hasura plugin uninstall myplugin   # succeeds","handlingStrategy":"validation","validationCode":"func isSymlinkOrMissing(path string) bool {\n    fi, err := os.Lstat(path)\n    if errors.Is(err, fs.ErrNotExist) { return true }\n    return err == nil && fi.Mode()&os.ModeSymlink != 0\n}\n// call before Uninstall/createOrUpdateLink\nif !isSymlinkOrMissing(dst) { /* move/delete the regular file yourself */ }","typeGuard":"func isSymlink(path string) bool {\n    fi, err := os.Lstat(path)\n    return err == nil && fi.Mode()&os.ModeSymlink != 0\n}","tryCatchPattern":"// treat as a warning: the CLI is protecting a real file\nif err := uninstall(); err != nil && strings.Contains(err.Error(), \"is not a symlink\") {\n    // manually inspect/remove the file, then retry\n}","preventionTips":["Only let the CLI manage files in the plugins bin directory.","Audit the bin dir periodically: find ~/.hasura/plugins/bin ! -type l."],"tags":["go","filesystem","symlink","safety-check","plugin-uninstall"],"backgroundTag":"path-is-not-a-symlink","analyzedSha":"724551b9ae87845594ef0408cff0e50eb6c90dc5","analyzedAt":"2026-08-28T07:32:55.105Z","schemaVersion":2},"datasetVersion":"2026-08-28T11:17:15.048Z"}