Write your own pack
A pack is one text file. Put it in a folder, and the palette offers its values beside the built-in ones. This page goes from an empty folder to your first value typed into a field.
What is a pack?
A pack is one text file in TOML, named after the pack, that holds test values in the order a tester meets them. Each value says what it is, what it usually breaks in an application and what a correct application does with it. This is a whole pack with one value:
format = 1
[pack]
id = "first-pack"
name = "First pack"
description = "Three values for a name field, to show how a pack is written."
version = "1.0"
updated = 2026-11-10
license = "CC-BY-4.0"
authors = ["Naughty Keyboard"]
language = "en"
tags = ["example", "names"]
fields = ["name", "text"]
risk = "normal"
[[values]]
id = "trailing-space"
name = "Name with a space at the end"
value = "Kowalski\u0020"
breaks = "The sign-up form keeps the space and the login form trims it, or the other way round, so the account is created and then cannot be found."
expect = "Trimmed everywhere or kept everywhere, so the same name always finds the same account."
since = "1.0"The lines down to the first [[values]] describe the pack, and each [[values]] block is one value. Text after a # is a comment, which the program skips. Text goes in double quotes, a number and a date do not, and a list is written in square brackets.
How do I make my first pack?
- Open your own folder. In the palette, press AltShiftSpace to open the value window, click Pack folders at the top, and then Create and open beside your own folder. The folder opens in your file manager.
- Put a template in it. Download
first-pack.tomlinto that folder, or one of the other pack templates. - Open the file in a text editor, such as Notepad, TextEdit or a code editor. A word processor saves formatting the program cannot read.
- Make it yours. Give the pack its own
idand rename the file to match, socheckout.tomlholdsid = "checkout". Then replace the example values with your own. - Use it. Press AltShiftSpace
again: your pack is in the list, marked
own folder. Choose it, click into the field you are testing and press AltShiftN .
The value window reads the folder each time it opens, so a change to the file shows without restarting the palette. A pack with a mistake in it is listed as not loading, with the number of problems, and checking a pack shows where they are.
If you use the command line, nkb new-pack checkout writes a new pack file with one example value, ready to edit, and the rest of this page applies to it the same way.
Where does the file go?
In your own folder, or in a team folder. Your own folder is packs beside the settings file:
- Windows:
%APPDATA%\Naughty Keyboard\packs - macOS:
~/Library/Application Support/Naughty Keyboard/packs - Linux:
~/.config/naughty-keyboard/packs
A team folder is any folder your team keeps pack files in, described below. In both, a pack file stands straight in the folder, not in a folder inside it, and its name is the pack’s id with .toml at the end. A folder may hold up to 256 pack files, and one file up to 4 MiB.
A pack with the same id as a built-in pack is used instead of it, and a pack in your own folder instead of one in the team folder. The palette, the value window and the report block say where a pack came from, so nobody types values from one pack while thinking of another.
What goes at the top of a pack?
The first line, format = 1, and then the [pack] table, which describes the whole pack:
| Field | Needed | What to write |
|---|---|---|
id | yes | The pack’s name for the program: lower case letters, digits and hyphens, starting with a letter. The file is named the same, with .toml at the end. |
name | yes | A short name people read in the palette, up to 120 characters. |
description | yes | One or two sentences on what the pack is for, up to 300 characters. |
version | yes | The version of the pack’s values, such as "1.0". Raise it when the values change. |
updated | yes | The day of the last change, as 2026-11-10, without quotes. |
license | yes | The licence of the values, such as "CC-BY-4.0". For a pack that stays in your team, write what your team uses, such as "proprietary". |
authors | yes | A list of names, such as ["Jan Kowalski"]. |
language | yes | The language the sentences are written in, such as "en" or "pl". |
tags | advised | Words the search of the value window finds the pack by. Without any, nkb lint warns: only somebody who knows the pack’s name finds it. |
fields | no | The kinds of field the values suit: any, text, multiline, name, username, password, email, url, search, number, integer, money, date, time, datetime, phone, postal-code, country-code, address, filename, path, id-number. |
risk | no | normal, offensive. A value marked offensive is marked in the palette before it is typed. |
source | no | Where the values came from, when they are somebody else’s work. |
What goes in a value?
One [[values]] block for each value:
| Field | Needed | What to write |
|---|---|---|
id | yes | The value’s own name: lower case letters, digits and hyphens, different from every other id in the pack. A report block cites a value as pack/id, so keep it once others use the pack. |
name | yes | A short label the palette shows. |
value | yes, for a value written out | The text that is typed, in double quotes. |
breaks | yes | What this value usually breaks, and why, in at least 40 characters. |
expect | advised | What a correct application does with it. Without it, nkb lint warns. |
since | yes | The version of the pack the value first appeared in. |
fields, tags, risk | no | The same as at the top, for this one value. |
type | no | A value the program works out: "repeat" for a very long value, "next-date" for a date that stays in the future. |
The palette types the values in the order they stand in the file, so the value most likely to find a bug goes first. A tester who presses AltShiftN
five times and moves on gets your five best values. A pack of more than 60 values works, and nkb lint suggests splitting it.
How do I write a good breaks sentence?
Say what goes wrong and why, so that the person who types the value knows where to look. breaks is the reason a pack is worth more than a list of strings, and it is the one part nobody can write for you.
- Too short, and refused by
nkb lint:Breaks the login. - Says what, not why:
Login fails with a trailing space. - Says what and why:
The sign-up form keeps the space and the login form trims it, or the other way round, so the account is created and then cannot be found.
expect is the other half: what a correct application does, so a tester can tell a bug from the right answer. For the value above: Trimmed everywhere or kept everywhere, so the same name always finds the same account.
How do I write a character nobody can see?
Write it as an escape inside double quotes, such as \u0020 for a space at the end. A character you cannot see survives one editor and vanishes in the next, and a reviewer cannot tell it is there at all. Everything you can see stays as it is: letters with accents, other scripts and emoji are written out.
| Write | For |
|---|---|
\u0020 | a space at the start or the end of a value |
\u00A0 | a non-breaking space |
\t | a tab, which the palette types with the Tab key |
\n | a new line, which the palette types with the Enter key |
\u200B | a zero-width space |
\u202E | a right-to-left override |
\uFEFF | a byte order mark |
\\ | a backslash |
\" | a double quote |
\U0001F600 | any character by its code point, with eight digits for one above \uFFFF |
A letter that looks like another, such as a Cyrillic o among Latin letters, is clearer escaped as well, as \u043E, so a reviewer sees that it is not an o.
Not sure which characters need it? Write the value as it is and run nkb lint: it names each character that needs an escape and gives the escape to write.
In single quotes nothing is an escape. That makes 'C:\temp' a path as it is written, and makes 'Kowalski\u0020' the letters \u0020, not a space. Use double quotes for anything with an escape in it.
How do I write a very long value?
Write a short piece and how many times it repeats, with type = "repeat", instead of the whole value:
[[values]]
id = "len-51"
name = "51 characters, one over the limit"
type = "repeat"
unit = "a"
count = 51
breaks = "A limit checked only in the browser lets it through when the request is sent another way, and a database column of 50 cuts the last letter or refuses the row with a raw error."
expect = "Refused with a message that names the limit, and never saved cut short."
since = "1.0"unit is the piece, which can hold escapes, and count is how many times it repeats, from 1 to 1,000,000. The whole value may be up to 1,000,000 code points. The palette types all of it, at the pace the application takes it, and Escape
stops it. A value written out that is longer than 2,000 characters and made of one piece repeated gets a warning from nkb lint that suggests repeat.
How do I write a date that stays in the future?
Write the rule of the day, with type = "next-date", instead of the date:
[[values]]
id = "spring-forward"
name = "Check-in at an hour the clocks skip"
type = "next-date"
month = 3
week = "last"
weekday = "sunday"
time = "02:30"
breaks = "On the night clocks go forward in the European Union this hour does not exist, so a form moves it to 03:30 without saying so, or fails with a raw error."
expect = "Refused with an explanation, or moved with the new time shown."
since = "1.0"When the value is typed, the program works out the first day that fits the rule and is at least two days after today in UTC, so it is in the future in every time zone. It types the day as YYYY-MM-DD, or YYYY-MM-DD HH:MM with a time. A date written out passes into the past one day and quietly stops testing what it says.
monthis 1 to 12.weekis the week of that month:first,second,third,fourth,last.weekdayismonday,tuesday,wednesday,thursday,friday,saturday,sunday.timeis optional, asHH:MMon a 24-hour clock.
The report block names the day that was typed, so a finding can be typed again exactly as it was.
How do I make a pair?
Write the two values one after the other, then a [[pairs]] block that joins them. A pair is two values typed into two fields, such as an e-mail and the field that repeats it:
[[values]]
id = "email"
name = "E-mail address"
value = "jan.kowalski@example.com"
breaks = "The first half of a pair, and on its own a plain address every form takes. Type it into the e-mail field, then the next value into the field that repeats it."
expect = "Accepted."
since = "1.0"
[[values]]
id = "email-trailing-space"
name = "The same address with a space at the end"
value = "jan.kowalski@example.com\u0020"
breaks = "The second half of the pair. A form that trims one field and not the other says the two addresses differ, or saves the address with the space and no mail arrives."
expect = "Trimmed in both fields, and accepted as the same address."
since = "1.0"
[[pairs]]
id = "repeat-email"
name = "E-mail and the field that repeats it"
relation = "identity"
a = "email"
b = "email-trailing-space"
breaks = "The two fields are compared before they are trimmed, so a person who typed the same address twice is told the addresses differ."
expect = "Both fields trimmed the same way, and the two accepted as one address."
since = "1.0"a and b are the id of the two values, and b stands right after a in the file, because the second press types whatever comes next. The palette shows pair 1 of 2 and pair 2 of 2 beside the name of each half. relation is one of look-alike, range, identity:
look-alike: two values that look the same and differ in their bytes, such as two logins in two alphabets.range: a start and an end, such as an evening start and a morning end.identity: two values the application should take as the same, or as different, such as a password and its repetition.
How do I check a pack?
Run nkb lint checkout.toml, with the name of your file. It checks the file against every rule of the format and lists every problem in one go, each on its own line, and ends with how many errors and warnings it found. The examples below show what it prints.
A line names the file, the line the problem is on, a code and a sentence that says what to do. A code starting with E is an error: the pack does not load until it is fixed. A code starting with W is a warning: the pack loads, and the line says what it would gain. Then nkb fmt checkout.toml lines the file up the way nkb lint expects, and never changes a value. nkb lint and nkb fmt describe every option.
nkb is the command line, the second program of every release. The Windows installer puts it on the PATH with the palette, and Download has its archive for each system.
What do the most common errors mean?
Each example below is a file with one mistake in it, and what nkb lint says about it, word for word.
The file name and the id differ
In file-name.toml:
3[pack] 4id = "my-shop" 5name = "My shop" 6description = "Values for the checkout of my shop." 7version = "1.0" 8updated = 2026-11-10 9license = "CC-BY-4.0" 10authors = ["Me"] 11language = "en" 12tags = ["checkout"]
What nkb lint says:
$ nkb lint file-name.toml
file-name.toml:4 E010 the pack declares id `my-shop`, which is not this file's name. A pack is addressed by its file name, so rename one of the two to match the other.
1 error, 0 warnings.
Checked 43 of 49 rules, 1 of them only in part. 5 not checked - run `nkb lint --explain` to see which and why.
The pack format is not frozen yet: a later release may still change it.
Rename the file or change id, so that the two are the same. The palette finds a pack by its file name.
A value has no breaks
In no-breaks.toml:
14[[values]] 15id = "trailing-space" 16name = "Name with a space at the end" 17value = "Kowalski\u0020" 18expect = "Trimmed everywhere or kept everywhere." 19since = "1.0"
What nkb lint says:
$ nkb lint no-breaks.toml
no-breaks.toml:14 E030 value `trailing-space` declares no `breaks`. It is the field this catalogue exists for: say what this value usually breaks and why, or the value is a curiosity rather than a test case.
1 error, 0 warnings.
Checked 43 of 49 rules, 1 of them only in part. 5 not checked - run `nkb lint --explain` to see which and why.
The pack format is not frozen yet: a later release may still change it.
Add breaks to the value, a sentence on what it breaks.
The breaks sentence is too short
In short-breaks.toml:
14[[values]] 15id = "trailing-space" 16name = "Name with a space at the end" 17value = "Kowalski\u0020" 18breaks = "Breaks the login." 19expect = "Trimmed everywhere or kept everywhere." 20since = "1.0"
What nkb lint says:
$ nkb lint short-breaks.toml
short-breaks.toml:18 E031 value `trailing-space` has a `breaks` of 17 characters, where the format asks for 40. Say what this value usually breaks and why - the sentence is the work this catalogue is made of.
1 error, 0 warnings.
Checked 43 of 49 rules, 1 of them only in part. 5 not checked - run `nkb lint --explain` to see which and why.
The pack format is not frozen yet: a later release may still change it.
Say what goes wrong and why, in at least 40 characters.
A space is written out
In space-written-out.toml:
14[[values]] 15id = "trailing-space" 16name = "Name with a space at the end" 17value = "Kowalski " 18breaks = "The sign-up form keeps the space and the login form trims it, so the account is created and then cannot be found." 19expect = "Trimmed everywhere or kept everywhere." 20since = "1.0"
What nkb lint says:
$ nkb lint space-written-out.toml
space-written-out.toml:17 E020 value `trailing-space` writes \u0020 out instead of escaping it. A reviewer cannot see it and an editor can drop it - write it as \u0020.
1 error, 0 warnings.
Checked 43 of 49 rules, 1 of them only in part. 5 not checked - run `nkb lint --explain` to see which and why.
The pack format is not frozen yet: a later release may still change it.
The value ends with a space nobody can see. Write it as \u0020, as the sentence says.
A value in single quotes needs an escape
In single-quotes.toml:
14[[values]] 15id = "trailing-space" 16name = "Name with a space at the end" 17value = 'Kowalski ' 18breaks = "The sign-up form keeps the space and the login form trims it, so the account is created and then cannot be found." 19expect = "Trimmed everywhere or kept everywhere." 20since = "1.0"
What nkb lint says:
$ nkb lint single-quotes.toml
single-quotes.toml:17 E022 value `trailing-space` needs \u0020 escaped, but is written in single quotes where nothing is an escape. Use double quotes and write \u0020.
1 error, 0 warnings.
Checked 43 of 49 rules, 1 of them only in part. 5 not checked - run `nkb lint --explain` to see which and why.
The pack format is not frozen yet: a later release may still change it.
Put the value in double quotes and write the space as \u0020.
The file is not valid TOML
In curly-quotes.toml:
14[[values]] 15id = "trailing-space" 16name = “Name with a space at the end” 17value = "Kowalski\u0020" 18breaks = "The sign-up form keeps the space and the login form trims it, so the account is created and then cannot be found." 19expect = "Trimmed everywhere or kept everywhere." 20since = "1.0"
What nkb lint says:
$ nkb lint curly-quotes.toml
curly-quotes.toml:16 E004 not valid TOML: string values must be quoted, expected literal string. Nothing else in this file could be checked.
1 error, 0 warnings.
Checked 43 of 49 rules, 1 of them only in part. 5 not checked - run `nkb lint --explain` to see which and why.
The pack format is not frozen yet: a later release may still change it.
The quotes around the name are curly ones, which some editors put in for straight quotes. TOML takes straight quotes only. Write them again, or use an editor that keeps them straight. The same error comes with a quote left open, or a bracket missing.
A field kind is not on the list
In wrong-field-kind.toml:
14[[values]] 15id = "trailing-space" 16name = "Name with a space at the end" 17value = "Kowalski\u0020" 18breaks = "The sign-up form keeps the space and the login form trims it, so the account is created and then cannot be found." 19expect = "Trimmed everywhere or kept everywhere." 20fields = ["e-mail"] 21since = "1.0"
What nkb lint says:
$ nkb lint wrong-field-kind.toml
wrong-field-kind.toml:20 E028 value `trailing-space` names field kind `e-mail`, which is not in the format's closed list: any, text, multiline, name, username, password, email, url, search, number, integer, money, date, time, datetime, phone, postal-code, country-code, address, filename, path and id-number. Pick the nearest one rather than coining one.
1 error, 0 warnings.
Checked 43 of 49 rules, 1 of them only in part. 5 not checked - run `nkb lint --explain` to see which and why.
The pack format is not frozen yet: a later release may still change it.
Pick the nearest word from the list in the sentence, here email.
A key has a typo
In typo-in-a-key.toml:
14[[values]] 15id = "trailing-space" 16name = "Name with a space at the end" 17value = "Kowalski\u0020" 18breaks = "The sign-up form keeps the space and the login form trims it, so the account is created and then cannot be found." 19since = "1.0" 20expected = "Trimmed everywhere or kept everywhere."
What nkb lint says:
$ nkb lint typo-in-a-key.toml
typo-in-a-key.toml:14 W032 value `trailing-space` declares no `expect`. Without it a tester sees what happened and has nothing to compare it against, so a bug and correct behaviour look the same.
typo-in-a-key.toml:20 E002 `trailing-space` declares `expected`, which the format does not define. A key nobody reads is a setting nobody applied - check for a typo.
1 error, 1 warning.
Checked 43 of 49 rules, 1 of them only in part. 5 not checked - run `nkb lint --explain` to see which and why.
The pack format is not frozen yet: a later release may still change it.
The format has no expected, so the value counts as having no expect at all. Correct the name, and both lines go.
The file is not lined up
In untidy.toml:
14[[values]] 15id = "trailing-space" 16name = "Name with a space at the end" 17value = "Kowalski\u0020" 18breaks = "The sign-up form keeps the space and the login form trims it, so the account is created and then cannot be found." 19expect = "Trimmed everywhere or kept everywhere." 20since = "1.0"
What nkb lint says:
$ nkb lint untidy.toml
untidy.toml:15 W064 this file is not in the shape `nkb fmt` writes. Run `nkb fmt` on it, and a difference in a pull request stays a difference of content rather than of style.
0 errors, 1 warning.
Checked 43 of 49 rules, 1 of them only in part. 5 not checked - run `nkb lint --explain` to see which and why.
The pack format is not frozen yet: a later release may still change it.
Run nkb fmt untidy.toml. It lines the equals signs up and puts the fields in order, and never changes a value.
Can my team share packs?
Yes. Keep the pack files in a folder your team shares, for example a qa-packs folder in the repository of the application you test. Each tester points the palette at it once: Pack folders in the value window, then Choose folder, or drop the folder on that window. The palette says more about the window.
A team folder on a network share that does not answer in 2 seconds is skipped, with a sentence that says so, and the other packs keep working. In a pipeline, --team-folder names the folder for one run, so a build can check the packs and use them:
$ for f in qa-packs/*.toml; do nkb lint "$f" || exit 1; done
$ nkb emit checkout --team-folder qa-packs --format json > checkout.json
Can I write a pack in my own language?
Yes. Set language to your language, such as "pl", and write name, description, breaks and expect in it. The names of the fields and the words of the closed lists, such as email or look-alike, stay as they are, because the program reads them.
Can I change a built-in pack?
Yes, by putting a pack with the same id in your own folder: it is used instead of the built-in one, and the palette says so. The files of the built-in packs are in the packs folder of the repository, to copy and change. A value that belongs in everyone’s catalogue is better suggested, with what it breaks and where you saw it.
Is the format final?
Not yet. A later release may still change it, and its notes will say how. nkb lint says so on every run, so that a file passing today is not read as a promise about tomorrow.