Specification

Linkumori CLN Format 1.0 (Final)

Status
Final
Version
1.0
Published
Oct 2, 2026
Editor
@Subham
Source
docs/cln-format-1.0.md

Implementation. Linkumori implements this document after v100.62.0. The rule-by-rule reference is filter-syntax.md; where the engine makes a choice this document leaves open, that page says so. The worked examples and vectors below are checked against the engine.

Status and scope

This document is CLN Format 1.0 in its final form. It supersedes the 1.0 text announced on 2 October 2026, and there is no compatibility mode for that earlier text.

It is written against the 1.0 announcement.

What it keeps. Every key, array and pattern of the earlier 1.0 text stays valid. A file written for that text loads without edits, so no converter is needed.

What it changes. Run order, duplicate handling, toggle ids, filter escaping, and the safety of redirects and remote rule files. There are ten changes in all.

No backward compatibility. Every rule in this document applies to every rule file. A file that relied on the earlier behavior runs differently; Changes for existing rule files lists the cases.

Summary of changes

1.0 final differs from the earlier 1.0 text in ten places, and all ten apply to every rule file.

#ChangeEarlier 1.0 text1.0 final
1Version keyNone documentedOptional top-level cln, value "1.0"
2order and stagesAn ordered entry runs before every unordered entry, raw rules includedorder is compared only inside its own stage; raw rules always run before field rules
3Whole-number matchPatternRuns ahead of its group, smallest number firstKeeps its array position like any other entry
4Position of a replacing definitionTakes the first definition's array index and drops its orderInherits both, unless it sets its own order or "order": null
5$removeparam value escapesA parameter whose name starts with a reserved character cannot be targetedA backslash makes the next character literal
6@@ match pointChecked against the URL after raw rules have runChecked against the URL as the provider received it
7Toggle idsBare id, no namespacing; a replaced definition's id switches nothingproviderKey::id; replaced ids resolve to the winning definition
8Redirect targetsUsed verbatim; no check statedMust be an absolute http or https URL
9TerminationThe cycle repeats on each new URL; no bound statedAt most 10 cycles per request; a rewrite applies once per request
10Remote file capabilitiesA remote file can redirect or block as soon as it loadsCapabilities are derived per file; redirect and block need acceptance

The earlier text is silent on rows 8 and 9, so 1.0 final writes them down.

File structure

A rule file gains one optional top-level key, cln, which names the format version. Its only valid value is "1.0".

JSON
{
  "cln": "1.0",
  "defaults": { "requestTypes": ["main_frame"] },
  "providers": {
    "shop": {
      "domainPatterns": ["||shop.example^"],
      "rules": ["utm_source"]
    }
  }
}

The top-level keys are now cln, providers, metadata and defaults. Only providers is required.

cln valueLoader behavior
absentLoaded as 1.0
"1.0"Loaded as 1.0
anything elseFile rejected

One 1.0. There is no separate value for the earlier 1.0 text. A file with no key and a file with "1.0" both get the rules in this document.

Why the key exists. It lets a later format version be told apart from 1.0 without guessing from the file's shape.

Why not version. The ClearURLs new rule format uses a top-level version, and Linkumori imports those files. A separate key keeps format detection unambiguous: version means ClearURLs, cln means CLN.

Rejection. A rejected remote file is not loaded and Remote Rules Health reports it as an error. A rejected built-in or custom file fails lint-rules.

Processing order

Raw rules always run before field rules, and order only sorts entries inside one stage. This settles the open question in the earlier 1.0 text: the raw to field boundary cannot be crossed.

The stage sequence for each matching provider is unchanged:

  1. exceptions (skip the provider if one matches)
  2. redirections
  3. fieldRedirections
  4. completeProvider
  5. rawRules
  6. rules and referralMarketing
  7. $removeparam filters

Stages 5 and 6 are sorted by one key, which gains a leading stage part:

(stage, orderGroup, order, rank, arrayIndex)
PartValue
stage0 for a rawRules entry; 1 for a rules or referralMarketing entry
orderGroup0 if the entry is a rule object with a numeric order; 1 otherwise
orderThe entry's own order. Compared only between two orderGroup 0 entries of the same stage. Negative numbers and fractions are allowed
rankWhich array the entry sits in, from the table below
arrayIndexThe entry's zero-based position in its own array, with no exceptions
Arraystagerank
rawRules00
rules10
rules, object with "referralMarketing": true11
referralMarketing12

Read the key left to right; the first part that differs decides.

A cleanup that must happen before a raw rule is written as a raw rule. A raw rule is a regex against the whole URL, so it can express any parameter removal.

Three things carry over from the earlier 1.0 text. Entries that are off are left out. $removeparam filters run after stage 6. Neither they nor @@ entries may carry an order.

Worked example

JSON
{
  "cln": "1.0",
  "providers": {
    "shop": {
      "domainPatterns": ["||shop.example^"],
      "rawRules": [
        "\\/ref=[^/?]*",
        { "id": "strip-jsessionid", "matchPattern": ";jsessionid=[^/?#]*", "order": 20 }
      ],
      "rules": [
        "utm_source",
        { "id": "token-rewrite", "matchPattern": "token", "replacePattern": "clean-§1§", "order": 5 },
        "fbclid",
        { "id": "aff-id", "matchPattern": "aff_id", "referralMarketing": true },
        { "id": "sid", "matchPattern": "sid", "order": 20 },
        "$removeparam=/^pk_/"
      ],
      "referralMarketing": ["tag"]
    }
  }
}

With "allow referral marketing" off, the provider runs in this sequence. The last column shows where the earlier 1.0 text put the same entry.

RunEntrySort keyEarlier position
stageorderGrouporderrankarrayIndex
1rawRules[1] strip-jsessionid0020012
2rawRules[0] the /ref= rule01–004
3rules[1] token-rewrite105011
4rules[4] sid1020043
5rules[0] utm_source11–005
6rules[2] fbclid11–026
7rules[3] aff-id11–137
8referralMarketing[0] tag11–208

The $removeparam=/^pk_/ filter runs after row 8. In the earlier 1.0 text, token-rewrite read the URL before either raw rule had cleaned it. Here both raw rules finish first.

Run-order calculator

Paste a CLN rule file and pick a provider to see the sequence its rawRules, rules and referralMarketing entries run in under this document's sort key. It applies the rules above and in Same text twice; it runs in your browser and sends nothing anywhere. The sample is the worked example.

JSON
RunEntrySort keyNotes
stageorderGrouporderrankarrayIndex

Simulate a URL

Runs the provider's entries on a URL in the order above, one step at a time, pass after pass while the URL changes. It's a model of the order, not the engine: the provider's patterns and exceptions, $removeparam modifiers and preprocessors aren't checked. The custom rules editor in Linkumori simulates with the engine itself.

Whole-number matchPattern entries

An entry written like "3" or "20" now runs at its array position, the same as any other entry.

In the earlier 1.0 text these entries jumped ahead of their group, smallest number first. The cause is recorded in the 1.0 docs update: the engine keys these lists by text, and JavaScript lists number-like keys first. 1.0 final removes the two whole-number rows from the rank table and makes arrayIndex the array position in every case.

Engine requirement. An engine that keys rule lists by text must use a structure that keeps insertion order for every key, such as a Map. A plain JavaScript object does not qualify.

Lint. The warning for a whole-number entry without an order is withdrawn.

Worked example

JSON
{
  "cln": "1.0",
  "providers": {
    "numbers": {
      "domainPatterns": ["||numbers.example^"],
      "rawRules": ["r", "3"],
      "rules": ["a", "20", { "matchPattern": "9", "referralMarketing": true }, "b"],
      "referralMarketing": ["rm", "5"]
    }
  }
}

With "allow referral marketing" off:

RunEntrySort keyEarlier position
stageorderGrouporderrankarrayIndex
1rawRules[0] "r"01–002
2rawRules[1] "3"01–011
3rules[0] "a"11–006
4rules[1] "20"11–015
5rules[3] "b"11–037
6rules[2] "9"11–124
7referralMarketing[0] "rm"11–208
8referralMarketing[1] "5"11–213

Every entry runs where the file lists it. Rows 6 to 8 follow the unmarked rules entries because of rank, not because of their text.

Same text twice

A definition that replaces an earlier one now keeps the earlier one's position, order included, unless it sets its own.

The groups are unchanged: raw (rawRules), field (rules entries without the referral mark), and referral (marked rules objects, then referralMarketing). Two entries with the same matchPattern text in one group are still one entry. The last definition that isn't off still wins whole: its id, its replacePattern and every other key.

Two things change.

Position. A winning definition without its own order takes two things from the earlier definitions that aren't off. Its arrayIndex comes from the first one. Its order comes from the nearest earlier one that has an order key. Under the earlier 1.0 text it took the arrayIndex and lost the order.

Ids. The id and aliases of every replaced definition resolve to the winner. Under the earlier 1.0 text, toggling them switched nothing. See Rule ids and toggles.

First definitionLater definitionEarlier 1.0 text1.0 final
token, order: 1"token"No order; first definition's indexorder: 1; first definition's index
token, order: 1token, order: 50order: 50; own indexSame as earlier
token, order: 1token, "order": nullNot definedNo order; first definition's index
"token"token with a replacePatternRewrite, at first definition's indexSame as earlier

"order": null is new. It means "no order, and do not inherit one". It is the explicit way to drop an order set by an earlier definition.

Across field and referral. Unchanged in substance. While referral-marketing rules run, a referral entry with the same text as a field entry replaces it. Without its own order it takes the field entry's place, now including that entry's order.

Raw and field. Entries with the same text in the raw and field groups still do not collide; both run.

Within one file. Two different entries with the same text in one array remain a lint-rules error. Inheritance exists for merges across files, not as an authoring feature.

Overload mode

A remote copy of a built-in rule no longer drops the built-in order by accident, and a replaced rule is reported as a notice, not an error.

Four parts of overload mode are unchanged from the earlier 1.0 text:

Two parts change.

Position inheritance. The rule in Same text twice applies to the merged provider. A remote copy without an order keeps the order of the copy it replaces. To drop it, the remote copy sets "order": null.

Remote Rules Health levels. A rule replaced by a merge is a notice that names the replacing file. An error is reserved for a file or entry that did not load.

Built-inRemoteEarlier 1.0 text1.0 final
token, order: 1token, order: 50token at order: 50Same as earlier
token, order: 1"token"token with no ordertoken at order: 1
token, order: 1token, "order": nullNot definedtoken with no order
token, order: 1file 1: token, order: 20; file 2: token, order: 30token at order: 30Same as earlier
token, order: 1file 1: token, order: 20; file 2: "token"token with no ordertoken at order: 20
a order: 1, b order: 2b order: 1, a order: 2b, then aSame as earlier
fromBuiltIn, order: 5fromRemote, order: 5Both; fromBuiltIn firstSame as earlier
token order: 1 on site.exampletoken order: 50 on other.exampleNot merged: two providersSame as earlier

Rule ids and toggles

A toggle is now keyed by provider and rule together, as providerKey::id, so two providers can use the same rule id without sharing a switch.

The earlier 1.0 text keys the toggle off the rule id alone and does not describe namespacing. 1.0 final specifies it.

Worked example

The built-in rules define token with "id": "token-rewrite" in provider shop. A person switches shop::token-rewrite off. Later a remote file replaces that rule with a definition whose id is tok.

Earlier 1.0 text1.0 final
Rule that runsThe remote definitionThe remote definition
Effect of the saved toggleNone: the replaced id switches nothingThe rule stays off
Ids that switch the ruletokshop::tok and shop::token-rewrite

$removeparam and @@ exceptions

Any parameter name can now be targeted, and an @@ entry is matched against the URL the provider received.

Value escapes

A backslash in a $removeparam value makes the next character literal. In the earlier 1.0 text the first character after = picks the value form, so a parameter named ~foo, or one starting with |, could not be targeted.

Three rules bound the feature:

In a JSON rule file each backslash is written twice: "$removeparam=\\~foo".

Where @@ is matched

An @@ entry's pattern and modifiers are checked against the URL as the provider received it at the start of the current cycle. In the earlier 1.0 text they were checked after the provider's raw rules had run, so an exception scoped to a path that a raw rule deletes stopped matching.

JSON
{
  "cln": "1.0",
  "providers": {
    "shop": {
      "domainPatterns": ["||shop.example^"],
      "rawRules": ["\\/ref=[^/?]*"],
      "rules": [
        "$removeparam=tag",
        "@@||shop.example^/ref=$removeparam=tag"
      ]
    }
  }
}

For https://shop.example/ref=abc?tag=1:

Earlier 1.0 text1.0 final
URL the @@ pattern is checked againsthttps://shop.example?tag=1https://shop.example/ref=abc?tag=1
Does the exception matchNoYes
Resulthttps://shop.examplehttps://shop.example?tag=1

Unchanged

Redirect and block safety

A redirect may only go to an absolute http or https URL, processing always terminates, and a remote file cannot redirect or block until the person accepts that.

Redirect targets

A redirect target is valid when it parses as an absolute URL, its scheme is http or https, and it has a host.

The check covers all three sources of a target: the capture group of a redirections regex, the fixed target of a $redirect= entry, and the parameter value picked by fieldRedirections. It runs after the entry's preprocessors.

Termination

Processing a request ends after a bounded number of cycles. The earlier 1.0 text says the cycle repeats on each new URL and states no bound. Read literally, a rewrite such as clean-§1§ would apply again on every cycle.

Remote file capabilities

A remote file's capabilities are derived from its content when it loads. A file does not declare them.

CapabilityA file has it when it containsActive on load
stripA rules, rawRules or referralMarketing entry, or a $removeparam filterYes
rewriteAn entry with a replacePatternYes
exceptA provider exceptions entry or an @@ entryYes
redirectA redirections or fieldRedirections entry, or forceRedirectionAfter acceptance
blockA provider with "completeProvider": trueAfter acceptance

Conformance

An engine conforms to CLN 1.0 when it reproduces every worked example and table in this document and the vectors below.

Lint checks

CheckLevel
cln is present and is not "1.0"Error
An id or alias repeats within one providerError
A $redirect= entry's fixed target is not http or httpsError
Two different entries with the same text sit in one arrayError
A $removeparam filter or @@ entry carries an order, null includedError
A backslash precedes a character that has no escape, in a name valueError

The warning for a whole-number matchPattern without an order is withdrawn. Position no longer depends on it.

Run-order vectors

The worked examples under Processing order and Whole-number matchPattern entries are normative. So are the result tables under Same text twice, Overload mode and Rule ids and toggles.

Redirect vectors

The provider matches site.example and has "fieldRedirections": ["redirect"].

Request URLResult
https://site.example/away?redirect=https://real.example/pageRedirect to https://real.example/page
https://site.example/away?redirect=javascript:alert(1)No redirect: scheme is not http or https
https://site.example/away?redirect=ftp://files.example/xNo redirect: scheme is not http or https
https://site.example/away?redirect=//real.example/pageNo redirect: not an absolute URL
https://site.example/away?redirect=/local/pageNo redirect: not an absolute URL

In each "no redirect" row the provider's later stages still run on the request URL.

Termination vector

The token-rewrite rule from Processing order runs against https://shop.example/?token=abc123. The result is https://shop.example/?token=clean-abc123, and it does not change on any later cycle.

Changes for existing rule files

Every file written for the earlier 1.0 text is a valid 1.0 final file, so there is no converter and no compatibility mode. Five behaviors change, and a file that relied on one needs an edit.

What the file hasEarlier behaviorBehavior nowEdit, if the earlier behavior was intended
A rules or referralMarketing entry with an order, in a provider with raw rulesRan before unordered raw rulesRuns after every raw ruleRewrite it as a raw rule
A whole-number matchPattern without an orderRan ahead of its groupRuns at its array positionMove it earlier in its array
An @@ entry scoped to a path that a raw rule deletesDid not matchMatchesNarrow or remove the entry
A $removeparam name value that contains a backslashRead literallyRead as an escape; an unknown escape is a lint errorWrite the backslash twice
A remote copy of a built-in rule, without an orderDropped the built-in orderKeeps itAdd "order": null

Four engine rules also take effect for every file:

A toggle saved under a bare id no longer applies. Toggles keyed providerKey::id or by match pattern keep working.

Deferred to 2.0

1.0 final leaves the file layout alone, so six known problems stay open for 2.0.

Sources

Where the earlier 1.0 text is silent, this document says so and sets a rule.

Generated from docs/cln-format-1.0.md on the main branch. Found a problem? Open an issue.