juanfont/headscale · error

reading policy file: %w

Error message

reading policy file: %w

What it means

Thrown by `headscale policy set` when os.ReadFile on the --file flag value fails. The policy HuJSON file cannot be read before any validation or upload happens. The wrapped error is a standard os.PathError naming the exact reason (no such file, permission denied).

Source

Thrown at cmd/headscale/cli/policy.go:132

		fmt.Println(policyData)

		return nil
	},
}

var setPolicy = &cobra.Command{
	Use:   "set",
	Short: "Updates the ACL Policy",
	Long: `
	Updates the existing ACL Policy with the provided policy. The policy must be a valid HuJSON object.
	This command only works when the acl.policy_mode is set to "db", and the policy will be stored in the database.`,
	Aliases: []string{"put", "update"},
	RunE: func(cmd *cobra.Command, args []string) error {
		policyPath, _ := cmd.Flags().GetString("file")

		policyBytes, err := os.ReadFile(policyPath)
		if err != nil {
			return fmt.Errorf("reading policy file: %w", err)
		}

		if bypass, _ := cmd.Flags().GetBool(bypassFlag); bypass {
			d, err := openBypassDB(cmd)
			if err != nil {
				return err
			}
			defer d.Close()

			users, err := d.ListUsers(nil)
			if err != nil {
				return fmt.Errorf("loading users for policy validation: %w", err)
			}

			_, err = policy.NewPolicyManager(policyBytes, users, views.Slice[types.NodeView]{})
			if err != nil {
				return fmt.Errorf("parsing policy file: %w", err)
			}

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check the path in the error's wrapped PathError — use an absolute path.
  2. Verify readability: `ls -l` and `cat` the file as the same user running the CLI.
  3. In scripts, expand the path ($HOME/policy.hujson) instead of using ~.

Example fix

# before
headscale policy set --file ~/policy.hujson

# after
headscale policy set --file "$HOME/policy.hujson"
Defensive patterns

Strategy: validation

Validate before calling

func checkPolicyFile(path string) ([]byte, error) {
    if path == "" {
        return nil, fmt.Errorf("--file is required")
    }
    abs, err := filepath.Abs(path)
    if err != nil {
        return nil, err
    }
    fi, err := os.Stat(abs)
    if err != nil {
        return nil, fmt.Errorf("policy file missing: %w", err)
    }
    if fi.IsDir() {
        return nil, fmt.Errorf("policy path is a directory: %s", abs)
    }
    return os.ReadFile(abs)
}

Try / catch

policyBytes, err := os.ReadFile(absPath)
if err != nil {
    if errors.Is(err, os.ErrNotExist) {
        // typo in path: list candidate files, fix path
    } else if errors.Is(err, os.ErrPermission) {
        // fix ownership/run as file owner
    }
    return err
}

Prevention

When it happens

Trigger: `headscale policy set --file ./policy.hujson` where the path is wrong, a directory, or unreadable; tilde (~) not expanded because the flag value is passed to os.ReadFile literally.

Common situations: Relative path used from a different working directory; file created as root and CLI run as another user; typo in filename; using ~/policy.hujson unexpanded in a script.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/f6902bee06d1ebd9. Report an issue: GitHub.