juanfont/headscale · error

parsing policy file

Error message

parsing policy file: %w

What it means

Thrown in `headscale policy set --bypass...` when policy.NewPolicyManager(policyBytes, users, nodes) rejects the policy HuJSON. In bypass mode the CLI pre-validates structure and user references (the boot path, which deliberately skips test evaluation) before writing, so this fires on malformed HuJSON or references to nonexistent users.

Solutions

  1. Read the wrapped error — the v2 policy parser reports the exact path and position of the offending element.
  2. Cross-check every user/email in the policy against `headscale users list`.
  3. Run `headscale policy check --file <path>` against the live server to iterate faster before re-trying set.
  4. Validate HuJSON syntax with a HuJSON-aware formatter.

Example fix

// before: references a user that was deleted
{"grants":[{"src":["old-user@"],"dst":["tag:server"],"ip":["*"]}]}

// after
{"grants":[{"src":["current-user@"],"dst":["tag:server"],"ip":["*"]}]}
Defensive patterns

Strategy: validation

Validate before calling

// cheap pre-parse before the CLI's full validation
func hujsonParses(b []byte) error {
    var v any
    return json.Unmarshal(b, &v) // HuJSON with comments will fail; strip comments first or use hujson tooling
}

Try / catch

_, err := policy.NewPolicyManager(policyBytes, users, views.Slice[types.NodeView]{})
if err != nil {
    // parser error names path+position: fix file, re-run; do NOT fall back to writing unvalidated bytes to the DB
    return err
}

Prevention

When it happens

Trigger: Syntax errors in the HuJSON (missing comma, unbalanced braces); a "user" field naming a user that does not exist in the database; unknown autogroup/tag syntax; invalid CIDR in address rules; tag referenced without an owner.

Common situations: Editing policy by hand and referencing a deleted or renamed user; merging policies from another tailnet; trailing commas accepted in some HuJSON tooling but not others; policy written against a newer headscale syntax than the installed version.

Related errors


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

Appendix: source

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

		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)
			}

			_, err = d.SetPolicy(string(policyBytes))
			if err != nil {
				return fmt.Errorf("setting ACL policy: %w", err)
			}
		} else {
			policyStr := string(policyBytes)

			err := withClient(func(ctx context.Context, client *clientv1.ClientWithResponses) error {
				resp, err := client.SetPolicyWithResponse(ctx, clientv1.SetPolicyJSONRequestBody{
					Policy: &policyStr,
				})
				if err != nil {
					return fmt.Errorf("setting ACL policy: %w", err)
				}

				if resp.StatusCode() != http.StatusOK {

View on GitHub (pinned to 565fd254d0)