Skip to content
| Marketplace
Sign in
Visual Studio Code>Linters>Quack - Quick QANew to Visual Studio Code? Get it now.
Quack - Quick QA

Quack - Quick QA

Ryan Boylett

|
5 installs
| (0) | Free
Run custom QA Bills against your work directly in VSCode.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info
Quack - Quick QA

Quack - Quick QA

Version

Custom QA Bills for VS Code: small JavaScript or TypeScript rules that run against the active document, surface failures in a sidebar with jump-to selections, and offer one-click fixes - QA reviews you actually keep using because they live where you write.

Install

Open the Extensions view in VS Code (Ctrl/Cmd+Shift+X), search for Quack, hit Install - or grab it from the Marketplace.

Open any file, then click Check Bills in the Quack sidebar (or run Quack: Check Bills from the Command Palette) to evaluate every enabled bill.


Quick start

  1. Open Settings (Cmd+, / Ctrl+,) and search for quack.bills.
  2. Add a bill to the JSON array (see examples below).
  3. Open a file; the Quack sidebar will list your bills.
  4. Click Check Bills.

The Quack sidebar

The Quack activity-bar icon opens a sidebar split into four panels: Document, Side Effects, Bills, and Snippets. Each panel can be collapsed independently. Each panel exposes its search, sort, filter, and view-mode controls in the native title bar; click an action to open or focus the relevant in-panel UI.

When a file is active, the Bills panel shows:

  • Overall status: a coloured pill in the top bar reading Pass, Warning, or Fail based on the aggregate result of all checked bills.
  • Search: click the magnifying glass icon to filter bills by title or category.
  • Sort menu: click the list icon to choose how bills are ordered. Category (default) groups bills under collapsible category headings. Name shows a flat alphabetical list. Severity shows a flat list ordered fail -- warn -- pass -- unchecked. Last Modified shows a flat list ordered by bill file modification time, most recent first.
  • Filter menu: click the funnel icon to show/hide passing bills, failing bills, uncategorized bills, or individual categories.
  • Category groups: in Category sort mode, bills that share a category value are grouped under a collapsible heading. Each heading shows coloured count badges for failures, warnings, and passes. Bills without a category appear in an Uncategorized group at the top.
  • Bill cards: each bill shows its title, optional description, result message, and action links. Unchecked bills show a Check link to run that bill individually. Once checked, a refresh icon appears to the left of the action links to re-run just that bill. The card border is coloured green (pass), amber (warn), or red (fail/error).
  • Bill selection: clicking a bill card selects it (accent border, text switches to theme foreground). Each card is a single tab stop, so Tab / Shift+Tab moves selection between bills; tabbing in or out of the list clears the selection. Press Escape to clear the selection at any time. Keyboard shortcuts then apply to the selected bill -- see the table below.
  • Right-click menus: right-click a bill card for Fix/Check N Occurrences, Ignore N Occurrences, Edit Bill (opens the manifest in a new tab), Reveal in Finder/Explorer, and Enable/Disable. Right-click a side effect button for Run Side Effect, Edit Side Effect, and Reveal in Finder/Explorer.

Bill keyboard shortcuts

Shortcut Action
Cmd/Ctrl+Right Navigate to the next (or first) occurrence
Cmd/Ctrl+Left Navigate to the previous (or last) occurrence
Cmd/Ctrl+Enter Select all occurrences in the editor
Cmd/Ctrl+Shift+Enter Fix all, or the focused occurrence (granular bills)
Cmd/Ctrl+Backspace Ignore all, or the focused occurrence (granular bills)
Backspace Toggle the selected bill on or off (when focus is not in the editor)
Tab Move selection to the next bill
Shift+Tab Move selection to the previous bill
Escape Clear the selection

The Cmd/Ctrl shortcuts only fire while the Quack sidebar is visible.


Commands

Command Description
Quack: Check Bills Runs all enabled bills against the active document.
Quack: Check a Bill Prompts you to pick a single enabled bill, then runs its check.
Quack: Fix All Applies fix for every enabled bill that has it, sequentially.
Quack: Fix a Bill Prompts you to pick a single fixable bill, then applies its fix.
Quack: Run Side Effect Prompts you to pick a side effect from the available bills, then runs it.
Quack: Open Browser Panel Opens a live browser preview of the active document in a VS Code panel.
Quack: Open Documentation Opens this README in the VS Code Markdown preview.

All commands are available in the Command Palette (Cmd+Shift+P / Ctrl+Shift+P). You can also bind them to keyboard shortcuts via Preferences: Open Keyboard Shortcuts.


Missing module install prompt

When a bill's check, fix, or side effect throws a MODULE_NOT_FOUND error -- typically because the bill's node_modules haven't been installed -- Quack surfaces a notification listing the affected bill(s) and the missing module name(s). Click Install and Quack opens an integrated terminal in each affected bill's directory and runs npm install for you. The bills are ready to run again as soon as the install completes.


AI agent tools

Quack registers three VS Code language model tools that any LM-backed agent -- including GitHub Copilot and Claude -- can invoke when you ask it to run your QA rules. No additional configuration is required; the tools are registered automatically when the extension activates.

Tool Description
quack_list_bills Returns all bills loaded for the active document: IDs, titles, descriptions, categories, and whether each bill has a check and/or fix.
quack_check_bills Runs one or more bills against the active document and returns results (status, message, selection count, fix availability). Pass specific billIds or omit to run all enabled bills.
quack_fix_bill Applies the fix for a specific bill by ID to the active document.

Example prompts

The agent decides when to invoke these tools based on context. Prompts like the following will typically trigger them:

  • "Run my Quack checks on this file"
  • "Check the alt text bill"
  • "What QA bills are available for this document?"
  • "Fix all the bills that are failing and have a fix"
  • "Check bill missing-subject-line and fix it if it fails"

How checks and fixes work

When an agent calls quack_check_bills, Quack runs the same check logic used by the sidebar -- including the prepare phase, ignore filter, and scope matching. quack_fix_bill applies the same WorkspaceEdit that the Fix button uses, so the change lands in the editor immediately and can be undone.


Configuring Bills (quack.bills)

Each bill is a JSON object in your settings.json:

"quack.bills": [
  {
    "id": "lang-attr",
    "title": "HTML lang attribute",
    "description": "The <html> element must declare a language.",
    "category": "Accessibility",
    "check": "...",
    "fix": "..."
  }
]

Bill properties

Property Type Required Description
id string Yes Stable unique identifier. Used to track enabled/disabled state.
title string Yes Display name shown in the sidebar.
description string Short description of what the bill checks. Shown below the title (and as a tooltip in compact mode).
category string Group label used to visually group bills together in the sidebar. Bills with the same category are rendered under a shared collapsible heading. Bills without a category appear in the Uncategorized group.
order number Sort position within the sidebar. Bills with an order value are sorted numerically and appear before bills without one. Bills without an order are sorted alphabetically by title.
priority number \| string Fix order priority for Fix All. Lower values are fixed first. Accepts any integer, "Infinity", or "-Infinity". Bills without a priority are fixed last.
author string Author of the bill in Full Name <email@address.com> format.
repository string URL to the Git repository where this bill is maintained.
version string Decorative version label for the bill (semver). When set, Quack renders it as a small v<x.y.z> badge next to the bill title so users can see at a glance when a bill author has published an update. Quack does not gate bills by version.
scope string \| string[] File extension(s) or glob pattern(s) this bill applies to. Extensions are plain strings such as "html", "html,htm", or ["html","htm"]. Globs are any entry containing / or *, matched against the workspace-relative file path (e.g. "src/**/*.html"). When both extensions and globs are supplied the filter is generous -- a match on either passes. When only globs are supplied the filter is specific -- the path must match at least one glob. If omitted, the bill applies to all file types.
prepare string \| string[] JavaScript or TypeScript code that runs before the check phase. All preparations run in parallel. Must return an object with at least one key — the merged results are available in check as a top-level context variable. See Prepare.
check string \| string[] Yes JavaScript or TypeScript to evaluate the document. Can be inline code, an array of lines, or a path to a .js/.ts file. Supports export default.
fix string \| string[] JavaScript or TypeScript to fix the document. Can be inline code, an array of lines, or a path to a .js/.ts file. Supports export default.
granular boolean Set to true when the fix script reads this.target to operate on only the focused occurrence. Enables the Fix Selected and Ignore Selected labels when navigating between occurrences with the <</>> arrows. Without this, both buttons show their normal labels and always operate on all occurrences.
browser string \| string[] JavaScript or TypeScript that runs in the browser context each time the preview page loads. Has access to window and all browser APIs. Returns the same format as check. Results are not shown in the sidebar -- browser bills run silently in the background. Can be inline code, an array of lines, or a path to a .js/.ts file. See Browser Preview.
configuration object A VS Code-style settings schema declaring the bill's configurable properties. When present, a Configure icon appears on the bill card. See Bill Configuration.
sideEffects array Named action buttons shown in the sidebar's Side Effects section. See Side Effects.
analyser string \| string[] JavaScript or TypeScript that returns a flat key/value object whose entries appear as rows in the sidebar's Document panel alongside the built-in file/size/title/lang/charset rows. See Analyser scripts.
analyserLabels object Optional map of analyser keys to human-readable labels (e.g. { "wordCount": "Word count" }). Keys not listed fall back to the raw key name.
subscribers array Scripts that react to VS Code events (file saved, editor changed, files created, ...) or a 5-field cron schedule. Each entry has a name, an event or schedule, an optional scope/conditions, and a required effect script. See Bill subscribers.

Writing check

check runs when the user clicks Check Bills (or Quack: Check Bills). All enabled bills run in parallel.

All bill code fields (check, fix, prepare, conditions, effect) support TypeScript and export default. You can write bills in .ts files or use TypeScript syntax in inline code — Quack transpiles it automatically via esbuild. You can also use export default to return a value instead of a bare return statement.

Execution context

Inside check and fix, this is a context object with the following properties:

Property Value
this.body The full document source as a raw string
this.bill The bill object (id, title, description, etc.)
this.config Resolved configuration for this bill — an object of key/value pairs merged from the bill's configuration defaults and the user's workspace settings
this.filename Absolute path to the active document
this.match(regex) Finds all regex matches in the document and returns { match, groups, line, column, length, start, end }[] (see below)
context The merged object from all bills' prepare results. Empty {} if no bills define prepare. See Prepare
progress(value) Report loading progress on the bill card — pass a number (0–100) for a determinate bar, true for indeterminate, or falsy to clear (see below)
vscode The full VS Code extension API object — use for notifications, clipboard, commands, workspace access, etc.
Model The Apple on-device AI model wrapper (macOS-only). Exposes generate, stream, classify, extract, ocr, and multi-turn createSession. See Apple on-device AI.
this.toString() Returns this.body — allows this to coerce to a string in template literals and concatenation
require(id) CommonJS require — fs, path, and crypto are always available; fs and path are sandboxed to the active document's directory. Other modules resolve only from the bill's own directory

Return value

check must return one of the following:

Return value Result
{ result: 'pass' } Bill passed
{ result: 'pass', message: '...' } Bill passed, with a message
{ result: 'warn', message: '...' } Warning: bill is highlighted amber, document is not failed
{ result: 'fail', message: '...' } Bill failed: document is marked FAIL
true Shorthand for pass
false Shorthand for fail
(nothing / undefined) Quack shows a VS Code warning alert (treat this as a bug in your bill)

Any uncaught exception is caught by Quack and surfaced as an error result (treated as a failure).

selections

Include a selections array in the returned object to indicate exactly where in the document the problem was found. When present, a Jump To link appears in the bill's sidebar card. Clicking it applies all selections at once, so multiple occurrences can be highlighted simultaneously.

Each entry in selections is an object. You can specify positions using character indexes (start/end) or line/column coordinates — start takes priority when present. Entries can also carry a message and a type to drive grouped navigation in the sidebar (see below).

// Using line/column/length
return { result: 'fail', message: 'Missing lang attribute.', selections: [{ line: 1, column: 1 }] };

// Using character indexes (from this.match results, which include start and end)
const hits = this.match(/<font[\s>]/i);
return { result: 'fail', message: `${hits.length} <font> tags.`, selections: hits };

// Multiple occurrences with line/column
return { result: 'fail', message: '3 <font> tags found.', selections: [
  { line: 4, column: 3, length: 6 },
  { line: 9, column: 1, length: 6 },
  { line: 22, column: 7, length: 6 },
] };

Selection behaviour — character index mode:

start end Cursor behaviour
y y Selects from character start to character end
y — Places cursor at character start (no selection)

Selection behaviour — line/column mode (used when start is not present):

line column length Cursor behaviour
y — — Selects the entire line
y — 0 Places cursor at the start of the line (no selection)
y y — or 0 Places cursor at the column (no selection)
y y > 0 Selects length characters from the column

Per-selection messages and grouped navigation:

Any selection entry can include a message string and an optional type ("warn" or "fail"). When at least one selection carries a message, the sidebar switches to grouped navigation mode: selections are grouped by their message string, and each group is displayed as a labelled block with its own prev/next navigation:

Missing alt attribute
« Select 3 »

Deprecated <font> tag
« Select 2 »

The message is coloured according to type ("fail" is red, "warn" is amber). When no selections carry a message, the sidebar shows the standard single "Select N" link.

const hits = this.match(/<img(?![^>]*\salt=)[^>]*/i);
const deprecated = this.match(/<font[\s>]/i);
return {
  result: 'fail',
  selections: [
    ...hits.map(h => ({ ...h, message: 'Missing alt attribute', type: 'fail' })),
    ...deprecated.map(d => ({ ...d, message: 'Deprecated <font> tag', type: 'warn' })),
  ],
};

canFix

Return canFix: false to suppress the Fix Issue button even when the bill has fix. This is useful when the fix is only safe under certain conditions that the check can detect:

const { parse } = require('node-html-parser');
const items = parse(this.body).querySelectorAll('td[bgcolor]');
if (items.length === 0) return { result: 'pass' };

// Only offer to auto-fix if all occurrences use the same colour
const colours = new Set(items.map(el => el.getAttribute('bgcolor')));
return {
  result: 'fail',
  message: `${items.length} bgcolor attribute(s) found.`,
  canFix: colours.size === 1,  // fix is unsafe when colours differ
};

When canFix is omitted or true, the Fix Issue button is shown normally (subject to the bill having fix and the result being fail, warn, or error).

this.match(regex)

this.match searches the document for all matches of a regular expression and returns their positions. The regex does not need to be global; this.match always finds all matches.

Returns an array of match objects:

Property Type Description
match string The full matched string
groups string[] Captured groups (same as m.slice(1))
line number Line number of the match
column number Column number of the match
length number Character length of the match
start number Start index of the match
end number End index of the match

Example: fail on all <font> tags and select every occurrence:

const hits = this.match(/<font[\s>]/i);

if (hits.length > 0) {
  return {
    result: 'fail',
    message: `<font> tag found (${hits.length} total).`,
    selections: hits,
  };
}
return { result: 'pass' };

progress(value)

Report loading progress on the bill's sidebar card. A 2px accent-coloured bar appears at the bottom of the card. Useful for bills that perform slow operations (network requests, large file scans, etc.).

Call Effect
progress(50) Determinate bar at 50% width
progress(33.7) Accepts floats for fine-grained progress
progress(true) Indeterminate animated bar
progress(100) Bar fills to 100%, holds briefly, then fades out
progress(0) / progress(false) / progress(null) Clears the bar immediately

Progress is automatically cleared when the bill's result is returned. If the bar is at 100% when the result arrives, it fades out gracefully before the result is displayed.

const items = parse(this.body).querySelectorAll('img');
for (let i = 0; i < items.length; i++) {
  progress((i / items.length) * 100);
  // ... check each image ...
}
progress(100);
return { result: 'pass' };

require(id)

Bills can load Node modules from their own directory using require. This is useful for DOM parsing, utility libraries, or shared helper files.

require is sandboxed in two ways:

  • fs — all file-path arguments are validated against the active document's directory. Any path that resolves outside it throws an access-denied error.
  • path — path.resolve() is re-rooted to the active document's directory, so relative paths resolve there rather than against process.cwd().
  • Other modules — only modules installed in the bill's own directory can be loaded. Any resolution that escapes that directory throws an error.
// Load node-html-parser from the bill's directory (npm install node-html-parser in that directory)
const { parse } = require('node-html-parser');
const lang = parse(this.body).querySelector('html')?.getAttribute('lang');
return { result: lang ? 'pass' : 'fail', message: 'The <html> element must have a lang attribute.' };

Apple on-device AI (the Model class)

Bills running on macOS 26+ with Apple Intelligence enabled get access to the on-device foundation model (~3B parameters) via the Model global. Every call runs locally - nothing leaves the Mac, no API key, no network round-trip, no usage cost.

Model is shipped as a tiny bundled Swift binary (bin/quack-model) spawned on first use and shared across all bills.

Platform support

  • macOS 26 (Tahoe) or later with Apple Silicon
  • Apple Intelligence enabled in System Settings → Apple Intelligence & Siri
  • On Windows, Linux, or older macOS, Model is still defined but every method rejects with the reason unsupportedPlatform. Use Model.isSupported for a synchronous gate, or await Model.availability() for the full status (covers appleIntelligenceNotEnabled, deviceNotEligible, modelNotReady).
  • Model.ocr() only requires macOS -- it uses Apple's Vision framework and works even when Apple Intelligence is disabled.
if (!Model.isSupported) {
  return { result: 'warn', message: 'This bill needs Apple on-device AI (macOS only).' };
}

const status = await Model.availability();
if (!status.available) {
  return { result: 'warn', message: status.message };
}

Model.generate(prompt, options?)

One-shot text generation. Returns the model's reply as a string.

const summary = await Model.generate('Summarise this in one sentence:\n' + this.body, {
  temperature: 0.2,
  maxTokens: 80,
  instructions: 'You are a concise QA assistant. Return one sentence, no preamble.'
});

return summary.includes('TODO')
  ? { result: 'warn', message: 'Document mentions a pending TODO: ' + summary }
  : { result: 'pass' };

Options:

Property Type Description
temperature number Sampling temperature 0.0-2.0. Lower = more deterministic.
maxTokens number Cap on tokens in the response. Omit for the framework default.
instructions string System-style instructions that shape this call only.

Model.stream(prompt, options?)

Streaming generation. Iterate the returned object for delta strings as they arrive, or await stream.done for the full content.

const stream = Model.stream('Write a haiku about static analysis.');
let full = '';
for await (const delta of stream) {
  full += delta;
  progress(true);
}
return { result: 'pass', message: full };

// Or just await the full string:
const text = await Model.stream('...').done;

Model.classify(text, labels, options?)

Pick exactly one label from a list. The model is prompted with temperature: 0 and the result is snapped to the closest case-insensitive match.

const label = await Model.classify(this.body, [ 'invoice', 'receipt', 'contract', 'other' ]);
return { result: label === 'invoice' ? 'pass' : 'fail', message: `Detected: ${ label }` };

Model.extract(text, schema, options?)

Pull a structured JSON object out of free-form text using a JSON Schema.

const data = await Model.extract(this.body, {
  type: 'object',
  properties: {
    title: { type: 'string' },
    issues: { type: 'array', items: { type: 'string' } }
  },
  required: [ 'title' ]
});

return data.issues?.length
  ? { result: 'fail', message: `${ data.issues.length } issues in "${ data.title }"` }
  : { result: 'pass' };

Model.ocr(imagePath)

Runs Vision OCR on a local image file and returns any text found inside it. Uses Apple's Vision framework -- does not require Apple Intelligence, only Model.isSupported.

const path = require('path');
const docDir = path.dirname(this.filename);
const imgPath = path.resolve(docDir, 'header.png');

const { text, hasText } = await Model.ocr(imgPath);

return hasText
  ? { result: 'warn', message: `Image contains text: "${ text }"` }
  : { result: 'pass' };

Returns an object with:

Property Type Description
text string All recognised text joined into a single string. Empty string when nothing is found.
hasText boolean true when at least one text observation was returned by Vision.

Multi-turn sessions

createSession opens a stateful conversation with persistent instructions and history.

const session = await Model.createSession({
  instructions: 'You are a code reviewer. Reply with one short critique per turn.'
});

try {
  const a = await session.send('Review this snippet: ' + this.body);
  const b = await session.send('What is the most important issue from your previous critique?');
  return { result: 'warn', message: b };
}
finally {
  await session.close();
}

Sessions also support streaming via session.stream(prompt, options?). Always close() sessions when you're done - they hold context on the Swift side until released.

Output guarantees

  • Calls are serialised inside the Swift process; you can issue many concurrent await Model.generate(...) calls but they will be processed in order.
  • Every method respects the bill timeout (quack.billTimeout). For long-running models, raise the timeout or pin a timeout field on a side effect.
  • The model is deterministic at temperature: 0 for short prompts but not byte-stable across OS updates - don't snapshot outputs in tests.

Inline example

"check": "const hits = this.match(/<html(?![^>]*\\slang=)[^>]*>/i);\nreturn { result: hits.length === 0 ? 'pass' : 'fail', message: 'The <html> element must have a lang attribute.' };"

As an alternative to escaped newlines, check and fix can be an array of strings: each item is a line of code, joined before execution:

"check": [
  "const hits = this.match(/<html(?![^>]*\\slang=)[^>]*>/i);",
  "return { result: hits.length === 0 ? 'pass' : 'fail', message: 'The <html> element must have a lang attribute.' };"
]

File example

Point check at a .js or .ts file in your workspace (relative path, no semicolons):

"check": "./qa/checks/lang-attr.js"

Contents of ./qa/checks/lang-attr.js:

const hits = this.match(/<html(?![^>]*\slang=)[^>]*>/i);
return {
  result: hits.length === 0 ? 'pass' : 'fail',
  message: 'The <html> element must have a lang attribute (e.g. lang="en").'
};

TypeScript example

"check": "./qa/checks/lang-attr.ts"

Contents of ./qa/checks/lang-attr.ts:

const hits: { line: number; column: number; length: number }[] = this.match(/<html(?![^>]*\slang=)[^>]*>/i);

export default {
  result: hits.length === 0 ? 'pass' : 'fail',
  message: 'The <html> element must have a lang attribute.',
  selections: hits,
};

export default works in both .js and .ts files and is equivalent to a bare return.


Writing fix

fix is optional. It runs when the user clicks Fix Issue on a failed or warned bill, or when the user runs Quack: Fix All. The result is written directly back to the open document.

Quack: Fix All applies the fix for every enabled bill that has fix (and where canFix is not false), running them sequentially so each fix sees the output of the previous one.

By default, Quack re-checks bills after a fix is applied: fixing a single bill re-checks all previously checked bills (since a fix may affect other bills' results), while Fix All re-checks every enabled bill. This keeps results up to date without a manual re-check. Disable this with quack.checkAfterFix: false.

Execution context

Same as check: this is the context object, require is available, with one additional property:

Property Value
this.target When the user clicks Fix Selected after navigating to a specific occurrence with the <</>> arrows, this is the SelectionTarget for that occurrence ({ start, end, line, column, length }). null when the user clicked Fix or Fix N (fix all). Use this to limit the fix to a single occurrence

If your fix script does not check this.target, it will fix all occurrences regardless of whether the user clicked Fix or Fix Selected — which is the correct default behaviour for most bills.

To signal that your fix supports per-occurrence targeting, set "granular": true in the bill schema. This causes the sidebar to show Fix Selected and Ignore Selected (instead of Fix and Ignore) when the user has navigated to a specific occurrence. Bills without granular: true always show Fix and Ignore even in navigation mode.

Return value

fix must return the modified document string.

Example

// Inline fix: insert lang="en" if the attribute is missing
return this.body.replace(/<html(?![^>]*\slang=)/i, '<html lang="en"');

Prepare

Bills can define a prepare field — code that runs before the check phase. All preparations run in parallel, and their return values are merged into a single context object available to every check script. This is useful for expensive shared computation (e.g. DOM parsing) that multiple bills need.

Rules

  • prepare must return a plain object with at least one key. An empty object or non-object return is a fatal error — Quack aborts the check cycle and no bills are checked.
  • If two or more bills return the same key from their prepare, a fatal error occurs (duplicate key conflict).
  • Preparations do not run for fixes or side effects — only for checks.
  • The merged object is available in check as a top-level context variable (not on this).

Execution context

prepare receives the same this context as check (this.body, this.bill, this.config, this.filename, this.match, require, vscode), but does not receive progress or context.

Example

A bill that parses the DOM once and shares it:

{
  "id": "dom-parser",
  "title": "DOM Parser",
  "scope": "html",
  "prepare": [
    "const { parse } = require('node-html-parser');",
    "return { dom: parse(this.body) };"
  ],
  "check": "return true;"
}

Other bills can then use context.dom in their checks without re-parsing:

{
  "id": "lang-attr",
  "title": "HTML lang attribute",
  "scope": "html",
  "check": [
    "const lang = context.dom?.querySelector('html')?.getAttribute('lang');",
    "return { result: lang ? 'pass' : 'fail', message: 'Missing lang attribute.' };"
  ]
}

Result states

State Border colour Effect on document
pass Green Bill passed.
warn Amber Concern flagged. Document is not failed.
fail Red Bill failed. Document is marked FAIL.
error Red Exception thrown in check. Treated as a failure.

If any bill fails, the overall document status is Fail. If there are only warnings and no failures, the status is Warning. Otherwise the status is Pass.


Bill Imports (quack.billImports)

Instead of (or in addition to) defining bills inline in settings.json, you can use quack.billImports to load .json bill files by path, glob pattern, or directory. This is useful for sharing bills across projects or keeping them under version control independently.

"quack.billImports": [
  "./qa/bills/lang-attr.json",
  "./qa/bills/*.json",
  "./qa/shared-bills",
  "/absolute/path/to/bills/**/*.json"
]

Each entry can be:

  • A path to a single file — loads exactly that bill.
  • A glob pattern — loads all matched .json files (e.g. ./qa/bills/*.json, ./qa/**/*.json).
  • A directory path — loads all .json files within the directory recursively.

Relative paths resolve from the workspace root. All matched bills are merged and loaded alongside any quack.bills entries. Quack watches the base directory of each pattern for changes: adding, editing, or deleting a JSON file refreshes the sidebar automatically.

Bill file format

Each .json file is a single bill. The structure is identical to an entry in quack.bills:

{
  "id": "lang-attr",
  "title": "HTML lang attribute",
  "description": "The <html> element must declare a language.",
  "check": "lang-attr.check.js",
  "fix": "lang-attr.fix.js"
}

The id field is optional. If omitted, the filename without the .json extension is used as the ID (e.g. lang-attr.json -> id lang-attr).

File resolution

When check, fix, or prepare is a relative .js or .ts path in an imported bill, it resolves relative to the directory containing that bill file. This means you can keep bill logic alongside the bill definition:

qa/bills/
+-- lang-attr.json
+-- lang-attr.check.ts
+-- lang-attr.fix.ts
+-- title-tag.json
+-- title-tag.check.js

File paths in bills defined via quack.bills (in settings.json) continue to resolve relative to the workspace root.


Enabling and disabling bills

Each bill has a toggle in the sidebar. Disabled bills are skipped when running Check Bills. The enabled/disabled state is stored per-workspace in .vscode/settings.json under quack.billsDisabled and persists across sessions. This setting is not shown in the Settings UI and is managed entirely by the sidebar toggles.


Bill Configuration

Bills can declare configurable properties using a configuration object that follows VS Code's settings schema format. When a bill has configuration, a gear icon appears in the bottom-right corner of its card in the sidebar. Clicking the icon opens .vscode/settings.json (creating it if needed) and pre-populates the bill's configuration section with default values so the user can see what is available and start editing immediately.

Declaring configuration

Add a configuration object to the bill. Each key is a setting name; the value is a schema descriptor:

{
  "id": "lang-attr",
  "title": "HTML lang attribute",
  "configuration": {
    "lang": {
      "type": "string",
      "default": "en",
      "description": "Expected value for the lang attribute on <html> (e.g. \"en\", \"fr\")."
    }
  },
  "check": [
    "const hits = this.match(/<html(?![^>]*\\slang=)[^>]*>/i);",
    "if (hits.length === 0) return { result: 'pass' };",
    "return { result: 'fail', message: `lang attribute missing — expected lang=\"${this.config.lang}\".` };"
  ],
  "fix": [
    "return this.body.replace(/<html(?![^>]*\\slang=)/i, `<html lang=\"${this.config.lang}\"`);"
  ]
}

Configuration schema properties

Each property in configuration can use the following fields:

Field Type Description
type string Data type: "string", "number", "boolean", "array", or "object"
default any Default value used when the user has not overridden the setting
required boolean When true, the user must supply a value in quack.billConfig before the bill will run. Bills with unsatisfied required properties are greyed out and excluded from checks and fixes
description string Short plain-text description shown in the settings file
markdownDescription string Markdown description (use instead of description for richer text)
enum any[] Allowed values
enumDescriptions string[] Human-readable labels for each enum value
minimum number Minimum value (numeric properties)
maximum number Maximum value (numeric properties)

Using this.config in check and fix

Configuration is available as this.config — a plain object of key/value pairs. Values are taken from the user's workspace settings if present, falling back to the schema defaults.

// this.config.lang resolves to "en" unless the user has overridden it
const hits = this.match(/<html(?![^>]*\slang=)[^>]*>/i);
if (hits.length === 0) return { result: 'pass' };
return { result: 'fail', message: `lang attribute missing — expected lang="${this.config.lang}".` };

Where values are stored

User-supplied values live in .vscode/settings.json under quack.billConfig:

{
  "quack.billConfig": {
    "lang-attr": {
      "lang": "fr"
    }
  }
}

This is workspace-scoped, so each project can have its own overrides independently of others.


Side Effects

Bills can define side effects — named action buttons that appear in a dedicated collapsible "Side Effects" section above the Bills list. A bill can provide side effects alongside checks and fixes, or only side effects. The section includes a search bar, a category filter (funnel icon), and a view mode toggle (layout icon) for filtering by category and switching between list and grid views. Side effects are sorted by category, then by name.

Each side effect is an object in the bill's sideEffects array:

Property Type Required Description
name string Yes Display name for the button
icon string Codicon icon name (e.g. "file-code", "refresh")
category string Category for filtering and search. If omitted, inherits the parent bill's category
scope string \| string[] File extension(s) or glob pattern(s) this side effect applies to. Follows the same rules as the bill-level scope field. Use "browser" to make this a browser-context side effect (see below). If omitted, the side effect appears for all file types
conditions string \| string[] JavaScript or TypeScript condition code. Return true to enable the button, false to disable it. Evaluated after each check cycle. If omitted, the button is always enabled
effect string \| string[] JavaScript or TypeScript code to execute when the button is clicked. If a string is returned, the document content is replaced. Supports export default. Required for non-browser side effects
browser string \| string[] JavaScript or TypeScript code to run in the browser context when this side effect is triggered. Has access to window and all browser APIs. Required when scope is "browser". See Browser Preview
timeout number Per-side-effect timeout in seconds. Overrides quack.billTimeout for this side effect. Set to 0 to disable the timeout entirely -- useful for long-running operations like image optimisation that should be allowed to complete in the background. If omitted, the global quack.billTimeout setting applies
accept string \| string[] HTML <input type="file"> style filter. When set, clicking the button opens a file picker filtered by accept, and the user can also drag files onto the button. The selected/dropped files arrive in the effect as this.files: File[]. Accepts comma-separated extensions (".png,.jpg"), MIME globs ("image/*"), specific MIMEs ("application/pdf"), or "*" for any file. See File pickers and drops
multiple boolean When true, the file picker allows selecting more than one file and the drop handler accepts multi-file drops. Otherwise only the first matching file is forwarded. Defaults to false

Execution context

Side effect code receives the same execution context as check/fix — the this context object, require, progress, and vscode. When the side effect declares an accept attribute, this.files is populated with File objects for every selected or dropped file (always present as an array, empty when no files have arrived). See File pickers and drops for the File shape.

Return value

effect can return:

Return value Effect
string Replaces the document content
{ body: string } Replaces the document content
{ selections: Array } Creates selections in the editor (same format as check selections)
{ body: string, selections: Array } Replaces the document, then creates selections in the updated content
(nothing) No document changes

When both body and selections are returned, the body is applied first so that selection positions refer to the new content.

Example

{
  "id": "html-tools",
  "title": "HTML Tools",
  "check": "return true;",
  "sideEffects": [
    {
      "name": "Minify HTML",
      "icon": "file-binary",
      "effect": [
        "return this.body.replace(/\\n\\s*/g, '');"
      ]
    },
    {
      "name": "Copy to Clipboard",
      "icon": "clippy",
      "conditions": "return this.body.length > 0;",
      "effect": [
        "await vscode.env.clipboard.writeText(this.body);",
        "vscode.window.showInformationMessage('Copied to clipboard!');"
      ]
    }
  ]
}

The Side Effects section can be hidden entirely with quack.showSideEffects: false. If no bills define side effects, the section does not appear.

File pickers and drops

Setting accept (and optionally multiple) on a side effect turns its button into a file-input target. The two invocation paths:

  • Click — opens a VS Code file picker filtered by accept. Multi-select is enabled when multiple is true.
  • Drag-and-drop — drop files from Finder/Explorer or VS Code's own file tree onto the button. Files are filtered against accept in the webview; only matching files are forwarded.

The selected/dropped files land on this.files as an array of File objects. The shape mirrors the browser File API, plus a Node-specific path so bills can hand the absolute path to native tools without re-reading:

Member Type Description
name string The file's basename (no directory)
path string Absolute path on disk - useful for sharp, child_process, etc.
size number File size in bytes
type string Inferred MIME type, or '' if unknown
lastModified number fs.stat mtimeMs
text() Promise<string> Reads the full contents as UTF-8
bytes() Promise<Uint8Array> Reads the full contents as bytes
arrayBuffer() Promise<ArrayBuffer> Reads the full contents as an ArrayBuffer
stream() fs.ReadStream Returns a Node readable stream over the file's bytes

The active document still populates this.body, this.filename, etc., so an effect can either work on the dropped files alone or combine them with the current document. When no document is open and the user invokes the side effect with files, this.body and this.filename are empty strings; the effect runs anyway.

accept syntax mirrors HTML <input type="file">: comma-separated entries of .ext (extension), image/* (MIME glob), application/pdf (specific MIME), or * for any file. The webview-side filter recognises extensions and MIME types; the picker dialog expands known MIME globs into VS Code filter extension lists.

{
  "id": "image-tools",
  "title": "Image tools",
  "sideEffects": [
    {
      "name": "Optimise images",
      "icon": "wand",
      "accept": "image/*",
      "multiple": true,
      "effect": [
        "const sharp = require('sharp');",
        "for (const file of this.files) {",
        "  await sharp(file.path).jpeg({ quality: 80 }).toFile(file.path + '.opt.jpg');",
        "}",
        "vscode.window.showInformationMessage(`Optimised ${ this.files.length } image(s)`);"
      ]
    },
    {
      "name": "Count words in text file",
      "icon": "list-ordered",
      "accept": ".txt,.md",
      "effect": [
        "const text = await this.files[ 0 ].text();",
        "const wordCount = text.match(/\\b\\w+\\b/g)?.length ?? 0;",
        "vscode.window.showInformationMessage(`${ this.files[ 0 ].name }: ${ wordCount } words`);"
      ]
    }
  ]
}

Browser-scoped side effects (scope: "browser") do not support accept; the file picker round-trip only applies to the Node-side effect path.

Drop size limits -- when a dropped file has no on-disk path (typically a drag from a web page), the webview base64-encodes the contents and the extension writes them to a unique sub-directory under os.tmpdir()/quack-drop-*/, cleaned up after the effect completes. Each file is capped at 100 MB and each batch at 250 MB; oversized drops are silently skipped client-side and reported as Dropped files exceeded the size limit server-side. Symlinks are rejected at read time. The this.files[*].path always points at a regular file Quack has been told about.


Analyser scripts

A bill can declare an analyser script that contributes additional rows to the Document section of the sidebar.

The script returns a flat key/value object. Each entry becomes a row alongside the built-in file/size/title/lang/charset rows. Values are rendered with the theme's link colour and tooltipped with the contributing bill's title. The reserved keys title, lang, and charset override the built-in HTML extraction; any other key is shown verbatim (or under its analyserLabels label if provided).

Analysers run in the standard bill sandbox with the usual vscode, require, Model, dialog helpers, and progress callback. They are subject to quack.billTimeout; thrown errors silently collapse to an empty contribution so one broken analyser cannot poison the panel.

{
  "id": "word-count",
  "title": "Word count",
  "scope": "html",
  "analyser": [
    "const words = this.body.replace(/<[^>]+>/g, ' ').match(/\\b\\w+\\b/g) || [];",
    "return { wordCount: words.length, readingTimeMin: Math.ceil(words.length / 200) };"
  ],
  "analyserLabels": {
    "wordCount": "Word count",
    "readingTimeMin": "Reading time (min)"
  }
}

Bill subscribers

Bills can react to VS Code events or fire on a cron schedule by declaring one or more entries in a subscribers array. Each subscriber has its own effect script and runs independently of the bill's check / fix lifecycle.

Each entry takes the following fields:

Field Type Required Description
name string Display label for logging and debugging.
event string Yes One of: onDidSaveTextDocument, onDidOpenTextDocument, onDidCloseTextDocument, onDidChangeTextDocument, onDidChangeActiveTextEditor, onDidChangeTextEditorSelection, onDidChangeConfiguration, onDidChangeWindowState, onDidCreateFiles, onDidDeleteFiles, onDidRenameFiles, or cron.
schedule string (when event is cron) 5-field cron expression: minute / hour / day-of-month / month / day-of-week.
scope string \| string[] Optional scope override for this subscriber (defaults to the bill's scope).
conditions string \| string[] Optional gate script returning a boolean. The effect runs only when truthy.
effect string \| string[] Yes The script that runs when the event fires.
icon, category string Cosmetic.

The effect (and conditions) script runs in the standard bill sandbox with the usual vscode, require, Model, dialog helpers, and progress callback. In addition:

  • event is the VS Code event payload (e.g. the TextDocument for onDidSaveTextDocument, the FileCreateEvent for onDidCreateFiles). For cron, event is null.
  • bill is a mutable proxy on the bill manifest. Property assignments are captured by BillOverrideStore and overlaid on the on-disk bill for the rest of the window session; they are cleared on the next extension reload (no on-disk writes).
{
  "id": "auto-save-warn",
  "title": "Warn on >100KB save",
  "subscribers": [
    {
      "event": "onDidSaveTextDocument",
      "scope": "html,htm",
      "effect": [
        "const size = Buffer.byteLength(event.getText(), 'utf8');",
        "if (size > 100_000) vscode.window.showWarningMessage(`Saved ${ (size/1024).toFixed(1) }KB`);"
      ]
    },
    {
      "event": "cron",
      "schedule": "0 9 * * 1-5",
      "effect": "vscode.window.showInformationMessage('Good morning — daily lint reminder')"
    }
  ]
}

Browser Preview

Click the Preview button in the Quack sidebar header (or run Quack: Open Browser Panel) to open a live preview of the active document in a VS Code panel. The document is served from a local HTTP server. The panel updates as you type (debounced 300 ms), serving the unsaved content directly from memory. When the file is saved, the memory buffer is cleared and the panel reloads from disk.

Set quack.useIntegratedBrowser to true to open the preview in VS Code's built-in Integrated Browser tab instead of Quack's bundled webview. Reloads, document-save refreshes, and sidebar browser-state updates still target the Integrated Browser tab; the setting silently falls back to the bundled panel on VS Code builds older than 1.109.

When the Browser Preview panel is focused, the sidebar will clear its bill list if the current file has no browser bills defined.

Browser bills

Add a browser field to any bill to run JavaScript or TypeScript inside the preview page on each load. Browser bill code runs in the live browser context and has access to window, document, and all other browser APIs.

Browser bills return the same format as check:

{
  "id": "layout-check",
  "title": "Layout check",
  "browser": [
    "const main = document.querySelector('main');",
    "return { result: main ? 'pass' : 'fail', message: 'Page must contain a <main> element.' };"
  ]
}

Results are not displayed in the sidebar -- browser bills run silently in the background on every page load.

If the bill defines a prepare field, the prepare phase runs automatically before the browser script executes. Its return value is available as context inside the browser bill, matching the variable name used in check.

Browser bills have access to the following top-level variables, mirroring the check/fix execution context as closely as possible:

Variable Value
body The full document source as a raw string
bill The bill object (id, title, description, etc.)
config Resolved configuration for this bill
filename Absolute path to the active document
context The merged object from all bills' prepare results
vscode.window.showInformationMessage(msg, ...items) Shows a VS Code info notification; returns a Promise resolving to the clicked item label, or undefined if dismissed
vscode.window.showWarningMessage(msg, ...items) Same, as a warning notification
vscode.window.showErrorMessage(msg, ...items) Same, as an error notification
vscode.revealInEditor(filePath, searchText) Opens a file in the editor and scrolls to the first occurrence of searchText

Browser-context side effects

Side effects with scope: "browser" target the browser context instead of the editor. They use a browser property (not effect) for their code, which runs inside the preview window. Browser side effects appear in the sidebar's Side Effects section only while the Browser Preview panel is open.

{
  "id": "browser-tools",
  "title": "Browser Tools",
  "sideEffects": [
    {
      "name": "Log performance",
      "icon": "pulse",
      "scope": "browser",
      "browser": [
        "const entries = window.performance.getEntriesByType('resource');",
        "console.table(entries.map(e => ({ name: e.name, duration: e.duration })));"
      ]
    }
  ]
}

Context menu

Right-clicking any element in the Browser Preview panel opens a context menu. The available items depend on the element type:

Item Available on Action
Reload All elements Reloads the preview page
Copy Text All elements Copies the element's innerText to the clipboard
Copy HTML All elements Copies the element's innerHTML to the clipboard
Copy Image URL Images (or children of images) Copies the image src to the clipboard
Copy URL Links (or children of links) Copies the link href to the clipboard
Copy Source Video/audio elements Copies the media src to the clipboard
Find in Code Elements with source data Selects the element's source range in the active editor
Inspect All elements Highlights the element with a 3-second blue outline, logs it to the browser console, and opens VS Code developer tools for the webview

Find in Code is only available when Quack can determine the element's position in the source file. It is supported for static HTML files served by Quack's built-in server. PHP and other dynamic modes do not currently support it.

CMD/CTRL+R

Press Cmd+R (Mac) or Ctrl+R (Windows/Linux) while the preview is focused to reload the page. This is equivalent to the Reload context menu item.

CMD/CTRL+click

Hold Cmd (Mac) or Ctrl (Windows/Linux) and click any link in the preview to open it in the system's default browser instead of navigating the preview.

Browser dialog APIs

Browser bill code runs after the page has fully loaded, so window.load listeners will not fire. Use top-level code instead.

As alert, prompt, and confirm are not available in the VS Code webview sandbox, Quack replaces them with VS Code equivalents:

Call VS Code equivalent
alert(message) Info notification (fire-and-forget)
await prompt(message, default) Input box -- returns the entered string, or null if cancelled
await confirm(message) Modal message -- returns true (OK) or false (Cancel)

Because prompt and confirm are asynchronous, bill code that uses them must await their return values:

{
  "id": "confirm-example",
  "title": "Confirm example",
  "sideEffects": [
    {
      "name": "Rename heading",
      "scope": "browser",
      "browser": [
        "const h1 = document.querySelector('h1');",
        "const text = await prompt('New heading text', h1?.textContent ?? '');",
        "if (text && h1) h1.textContent = text;"
      ]
    }
  ]
}

Snippets

The Snippets panel aggregates every snippet available to the active workspace and lets you drag any of them straight into the editor. Sources discovered automatically:

  • User -- the same snippet files VSCode reads (<user-snippets>/*.json and <user-snippets>/*.code-snippets).
  • Workspace -- .code-snippets files in the active workspace's .vscode/ directory.
  • Extensions -- snippets contributed by every installed extension via its contributes.snippets entry.
  • Bills -- snippets declared on Quack bills via the new snippets field (see below).

Views

Toggle between two layouts with the view-mode action in the panel title bar:

  • Tree view -- the snippet name sits next to the source icon with a smaller description line below it. Rows are grouped by source then by language/extension/bill.
  • Thumbnail grid -- a wider grid of tiles. A tile shows the snippet's icon (or its thumbnail image when set) plus the name as a two-line caption underneath. Thumbnails share a fixed 16:9 aspect ratio so the grid stays uniform.

Organising snippets

  • Custom folders -- click the New folder icon on a source row to create a folder inside it. The folder name is collected through a VSCode input box. Drag any snippet onto a folder header to move it; drag onto the source row, or onto the blank space below the tree, to put it back at the source root. The layout is stored globally in the editor's state, so it follows you across workspaces and machines that share the same VSCode profile.
  • Hide / Show hidden -- right-click a snippet for Hide or a folder for Hide Folder. Hidden ids are stored globally (across windows and workspaces). When anything is hidden a Show hidden entry appears in the panel's filter dropdown that brings every hidden item back into view.
  • Collapsed state -- expanded/collapsed state for every source root and folder is stored globally too, so the tree opens the same way across sessions.

Right-click menus

Target Items
Snippet card Copy Snippet Code, Reveal in Finder/Explorer, Hide/Show, Edit, Delete
Custom folder Hide Folder/Show Folder, Rename Folder, Delete Folder
Auto-group folder (per-extension / per-bill) Hide Folder/Show Folder

Edit opens the underlying snippet file (the bill manifest for bill snippets) and jumps the cursor to the line where the snippet is defined. Delete asks for confirmation and rewrites the source JSON in place; it is disabled for read-only sources such as installed extensions. Rename Folder and Delete Folder only appear on custom folders -- the per-extension and per-bill auto-groups are derived from their source labels and can't be renamed or removed. Deleting a folder moves its snippets back to the source root.

Adding a new snippet

User and Workspace source rows have a + action button. Clicking it:

  1. Opens (or creates) a language-scoped snippets file in the active editor: <languageId>.json for User snippets and <languageId>.code-snippets for Workspace snippets. With no active editor or a plaintext document, it falls back to snippets.json / snippets.code-snippets.
  2. Inserts a stub entry at the end of the root object using VSCode's snippet tab stops, so you can immediately tab through name -> prefix -> body -> description.
  3. Leaves the file dirty so nothing is written to disk until you save.

Drag-to-editor

Dragging a snippet from the panel into an open editor inserts its body at the drop point. The drag uses the application/vnd.quack.snippet+json MIME and the drop edit is tagged with the quack.snippet kind, so you can pin Quack as your preferred snippet drop handler:

"editor.dropIntoEditor.preferences": [
  "quack.snippet"
]

See VSCode's drop into editor documentation for the full grammar; entries can be scoped per language or per workspace, or used to push Quack down the priority list.


Augmenting snippets

Beyond the standard VSCode fields (prefix, body, description, scope), Quack supports four additional fields on any snippet entry. They all live in the same JSON file the snippet itself does. Use them in user, workspace, extension, or bill snippets -- the loader treats every source the same way.

Field Type Purpose
icon string A codicon name. Replaces the default symbol-snippet icon in the tree row and (when no thumbnail is set) the thumbnail tile.
thumbnail string An image URL (https://...), data URI (data:image/...;base64,...), or local path. Relative paths resolve against the snippet file's directory (or the bill's base path for bill snippets). When set, the image replaces the icon inside the tile and the name renders as a two-line caption below.
prepare string | string[] JavaScript or TypeScript that runs before the snippet body is inserted. Can rewrite or replace the body.
transform string | string[] JavaScript or TypeScript that runs after the body has been "virtually" inserted into the document. Returns a replacement for the document, or a structured object describing a snippet insert plus an optional head insertion.

Example -- icon and thumbnail

// .vscode/components.code-snippets
{
  "Action card": {
    "scope": "html",
    "prefix": "card",
    "body": [
      "<article class=\"action-card\">",
      "  <h2>${1:Heading}</h2>",
      "  <p>${2:Body}</p>",
      "</article>"
    ],
    "icon": "preview",
    "thumbnail": "./previews/action-card.png"
  }
}

The image is loaded inside the webview via a vscode-resource:// URI, so any path under your workspace, the user snippets directory, the contributing extension, or the bill's directory is accessible without further configuration. Remote http(s):// URLs and inline data: URIs are also supported.

prepare

prepare is invoked with a context object describing the drop. Return a string to replace the body that will be inserted, or return nothing to leave the body unchanged.

Execution context (this):

Property Description
this.body The full text of the target document at drop time.
this.snippet The snippet content as a single string (joined with \n when authored as an array).
this.position Zero-based character offset of the drop position.
this.language The target document's languageId.
this.toString() Returns this.snippet, so a prepare that returns ${this} still resolves to the snippet content.
{
  "Timestamped log": {
    "prefix": "log",
    "body": [ "console.log('[${1:tag}]', ${2:value});" ],
    "prepare": [
      "const tag = new Date().toISOString();",
      "return this.snippet.replace('${1:tag}', tag);"
    ]
  }
}

A prepare-only snippet is inserted as a regular SnippetString, so VSCode's tab stops still work after the body is rewritten.

transform

transform runs after prepare (if any) and operates on the entire document with the snippet body already merged at the drop position. It can return a string, a structured object, or nothing (to fall back to a plain snippet insert).

Use transform when an insert needs to touch the file outside the snippet itself -- adding an import, registering the snippet in a manifest, sorting a list it landed in, etc.

Execution context (this):

Property Description
this.body The full document text with the snippet already inserted at this.position.
this.snippet The (possibly prepare-rewritten) snippet content.
this.position The drop offset, in the merged document.
this.language The target document's languageId.
this.toString() Returns this.snippet.

Return shapes:

Return Behaviour
string Replaces the entire document. Tab stops are not honoured because the document is a fully resolved string.
{ bodySnippet: string } Replaces the entire document with bodySnippet parsed as a SnippetString. Tab stops anywhere in bodySnippet are active and linked across the whole document -- use this when you need to insert into the snippet body and somewhere else (e.g. a matching import) while keeping a single linked tab-stop session.
{ snippet: string, headInsertion?: { position: number, text: string } } Inserts snippet as a SnippetString at the drop point (tab stops work), and optionally inserts headInsertion.text (with a trailing newline) at headInsertion.position -- handy for dropping an import at the top of the file while the cursor lands inside the snippet.
undefined Falls back to the default plain snippet insert.
{
  "Named export": {
    "prefix": "exp",
    "body": [ "export const ${1:name} = ${2:value};" ],
    "transform": [
      "const updated = this.body;",
      "if (updated.includes(\"export *\")) return undefined;",
      "return updated.replace(/(\\nexport \\{)/, '\\nexport { ${1:name},' );"
    ]
  }
}
{
  "Use hook": {
    "prefix": "uh",
    "body": [ "const ${1:value} = use${2:Hook}();" ],
    "transform": [
      "if (this.body.includes(\"import { use\")) return undefined;",
      "return {",
      "  snippet: this.snippet,",
      "  headInsertion: { position: 0, text: 'import { use' + '${2:Hook}' + ' } from \\'./hooks\\';' }",
      "};"
    ]
  }
}

Loading from external files

Both prepare and transform accept either inline source (string or string[]) or a relative path to a .js / .ts file:

{
  "Module skeleton": {
    "prefix": "mod",
    "body": [ "export const ${1:name} = {};" ],
    "prepare": "./snippets/prepare-module.ts",
    "transform": "./snippets/transform-module.js"
  }
}

Paths resolve relative to the snippet file (or the bill directory for bill snippets). The scripts run with the same sandboxed require Quack bills use, so they can import other workspace modules.

Bill-contributed snippets

A QABill can declare its own snippet entries via a snippets field. The shape is identical to a VSCode snippet object, with the same Quack additions:

// .quack/snippet-bill.json
{
  "id": "ui.cards",
  "title": "UI: Cards",
  "snippets": {
    "Action card": {
      "scope": "html",
      "prefix": "card",
      "body": [
        "<article class=\"action-card\">",
        "  <h2>${1:Heading}</h2>",
        "  <p>${2:Body}</p>",
        "</article>"
      ],
      "icon": "preview",
      "thumbnail": "./previews/action-card.png"
    }
  }
}

Bill snippets appear under the Bills source root in the panel, grouped by bill title. Relative thumbnail, prepare, and transform paths resolve against the bill's directory.


Settings reference

Setting Type Default Description
quack.bills array [] Bills defined inline in settings.json.
quack.billImports array [] Glob patterns, file paths, or directory paths pointing to .json bill files. Bill code can reference .js or .ts files.
quack.workspaceBills boolean false When enabled, automatically loads .json bill files from the .quack directory at the workspace root (top-level only). Script paths within each bill resolve relative to the .quack directory.
quack.billsDisabled array [] IDs of bills that are currently disabled. Written to .vscode/settings.json (workspace scope). Managed by the sidebar toggles -- not shown in the Settings UI.
quack.autoCheck boolean false Re-run all enabled bills automatically after each edit (debounced 500 ms).
quack.checkAfterFix boolean true Re-check bills automatically after applying a fix. Fix on a single bill re-checks all previously checked bills; Fix All re-checks every enabled bill.
quack.showSideEffects boolean true Show the Side Effects section in the sidebar.
quack.compactMode boolean false Condense bill cards: hides descriptions and reduces padding. Bill descriptions are still visible as tooltips.
quack.fileExtensions array [] File extensions Quack activates on (e.g. "html", ".js"). Leave empty to activate on all file types.
quack.billTimeout number 60 Maximum number of seconds a bill's prepare, check, fix, or side effect is allowed to run before it is cancelled and reported as an error. Set to 0 to disable the timeout.
quack.useIntegratedBrowser boolean false Route the browser preview through VS Code's built-in Integrated Browser (workbench.action.browser.open) instead of Quack's bundled webview panel. Requires VS Code 1.109 or newer; silently falls back to the bundled panel on older builds.

Support

If this is useful to you and you'd like to support its development, you can buy me a coffee on Ko-fi - always optional, always appreciated.

Support me on Ko-fi

License

MIT - see LICENSE.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft