← gettack.dev

Documentation

Tack is a script you add to a page so a human can point at things and an AI agent can find them again. Everything below is the whole product; there is nothing else to configure.

Install Activate Notes Rewrite text Adjust styles Multi-select Annotate an area Source hints Share a review Verify Export format Keyboard JavaScript API Events Privacy Limits

Install

One tag, before the closing </body>. No build step, no npm dependency, no framework requirement.

<script src="https://unpkg.com/@kibkalo/tack"></script>

For anything you ship to real users, pin the version so a future release cannot change behaviour under you:

<script src="https://unpkg.com/@kibkalo/tack@0.6.0"></script>

Serious deployments should self-host the file instead of loading it from a CDN. It is one file with no dependencies — copy tack.min.js into your own assets.

Where the tag goes

StackFilePosition
Plain HTMLindex.htmlBefore </body>
Next.js App Routerapp/layout.tsxInside <body>, after {children}
Next.js Pages Routerpages/_document.tsxInside <body>, after <Main />
Astrosrc/layouts/*.astroBefore </body>
Vite (any framework)index.htmlBefore </body>
Nuxtapp.vue or nuxt.config.tsapp.head.script
SvelteKitsrc/app.htmlBefore </body>
Railsapp/views/layouts/application.html.erbBefore </body>
Django / Jinjatemplates/base.htmlBefore </body>
Laravelresources/views/layouts/app.blade.phpBefore </body>
WordPresstheme footer.phpBefore wp_footer()

Pages you do not control

Use the bookmarklet from the home page: drag it to your bookmarks bar and click it on any page. It injects the same script. Sites with a strict script-src Content-Security-Policy will block it — that is a browser decision Tack cannot work around.

Activate

Add #tack to the URL, or press ⌘⇧F. No reload needed. The toolbar appears in the bottom-right corner.

Until then the script registers one idle keyboard listener and stops: no toolbar, no elements added, no network requests, nothing read from the page. That is why it is safe to leave in a production build.

Notes

Click any element to annotate it. Numbered markers on the page match the numbering in the export.

Copy exports this page's notes and clears them. You handed them to the agent; they should not come back in the next export. Undo sits in the toast, and ☰ → Restore last export brings the whole batch back.

Notes accumulate per page as you navigate, including across SPA navigation. ☰ → Copy all pages bundles everything into one file grouped by URL. Download .md never clears anything.

While Tack is active it catches clicks and hovers so the page underneath does not open dropdowns or follow links. Turn that off in ☰ → Settings when you want to select text instead.

Rewrite text in place

Click a text element, open the ✎ Edit text tab, and its current text is already in the box. Change it and the export carries an exact replacement instead of a description:

**Current:** `Building the future, one widget at a time.`
**Change to:** `Ship your first widget in five minutes.`

The same works for alt, placeholder, title, aria-label, href and value; pick the target in the dropdown when an element has several.

The export tells the agent to apply these verbatim, and to stop and report instead of overwriting if the current value no longer matches what you saw. That check matters when a review sits for a day before anyone acts on it.

Adjust styles

Open ◨ Style on any element and change its size, line height, weight, colour, background, padding, gap or radius. The page updates as you type, so you are judging the real thing rather than imagining it. Save, and the page goes back to how it was — the note carries the before and after, not the change.

The values you see are the browser's computed styles, so the note is exact. The export tells your agent to apply the change where the style is authored and to keep whatever the codebase already uses: a note that reads 16px → 24px must not turn var(--space-4) into a hard-coded pixel value.

Because the before and after are exact, style notes are checked properly by the applied pass, the same way text rewrites are.

How the preview avoids touching your element

It is a paused Web Animation held at its final frame. That sits above the author cascade and above inline styles, reaches inside an open shadow root, and leaves nothing behind when it is cancelled — the element gains no attribute and no inline style, and any inline style it already had survives untouched.

One thing it cannot beat is !important. Rather than silently showing you nothing, the row says locked by !important and the note records what you asked for, so your agent knows the rule has to go.

Multi-select

Shift-click several elements, or drag a box around them, then write one note for the group. The export lists every selector the note applies to under **Also applies to:**.

Annotate an area

Not all feedback is about an element. Drag a box that lands on nothing in particular — whitespace, a gap between cards, a strip that feels cramped — and instead of doing nothing, Tack records the area itself.

The note carries the box in page coordinates, the deepest element that contains it under **Inside:**, and what falls inside it under **Covers:**. The export tells the agent to work out which of those the note is really about rather than assuming all of them. Below roughly 24×24 pixels a drag is treated as a slipped click and ignored.

Region notes travel in a review link like any other. They are skipped by the applied check: an area has no expected value, so there is nothing there that could honestly be called verified.

Source hints

Some dev servers stamp the file that rendered an element onto the DOM. When one is there, Tack reads it and puts it at the top of the note as **Source:**, ranked above the selector.

data-astro-source-file + data-astro-source-locAstro, automatically in dev
data-inspector-relative-path + -line + -columnreact-dev-inspector
data-v-inspectorvite-plugin-vue-inspector
data-tsd-sourceTanStack Devtools
data-component-path + data-component-lineLovable-style tooling

Astro emits these by default in development. React and Vue do it only when their inspector plugins are installed, so expect the hint to be missing more often than present — it costs nothing when it is. If the element has no stamp, Tack walks up a few ancestors and labels the result as coming from one.

It stays a hint. The value comes from the page, so the export tells the agent to confirm the path exists in the workspace before editing it and never to touch anything outside the workspace on the strength of it. These paths can be absolute and carry a username and directory layout, so they are deliberately left out of review links — local exports only.

Share a review as a link

☰ → Copy review link compresses the whole review into the URL fragment:

https://acme.com/pricing#tack=zVY9b8IwEP0rlmdESttD…

Open it anywhere and the notes reappear on the real page. Nothing is uploaded — the review travels inside the link, so the person receiving it needs no account, no backend and no install.

Measured capacity, with notes that all differ: 10 notes fit in about 700 characters, 25 in about 1,000, 50 in about 1,500, 100 in about 2,500. Past roughly 2,000 characters some chat apps truncate long URLs; Tack warns you and suggests Download .md instead. A truncated link imports nothing and says so rather than importing half a review.

Verify what was applied

Tack records what each annotated element looked like when you flagged it. After your agent has finished, reload and choose ☰ → Check what was applied: changed elements go green, unchanged stay amber, missing ones are called out. Text rewrites are matched exactly against what you typed.

It compares the last export, so run it after Copy, not before.

Know what the two kinds of note prove. A text or attribute rewrite is checked exactly: green means the value is now the string you asked for. A freeform note has no expected value to compare against, so green only means the element's text is no longer what it was — an unrelated edit turns it green too, and a CSS or layout fix leaves it amber even when the note was addressed. Treat freeform results as a prompt to look, not as proof.

Export format

The export is Markdown that explains to the model how to read itself. Each note carries three anchors, and the file states which to trust:

## How to use this file
Anchors per note, most reliable first:
1. **Text** — element text at review time. Grep this first.
2. **Section** — nearest preceding heading. Disambiguates repeated text.
3. **Selector** — DOM path at review time; exact but stale after a refactor.

Resolve by Text, confirm with Section, fall back to Selector. If nothing
matches, the markup changed: act on the note's intent, do not guess a nearby
element. Edit where the markup is authored (component, template, partial),
not in built output.

Selectors break on the first refactor; element text usually survives. Saying so up front is worth more than another field of metadata.

Note bodies are quoted and framed as human-typed data, so a note that happens to read like an instruction is not mistaken for one.

>>> in a selector marks an open shadow-DOM boundary.

Keyboard

⌘⇧F / Ctrl⇧FToggle Tack on and off
⌘↵ / Ctrl↵Save the note you are typing
↑ / ↓Move the target to the parent or first child element
EnterAnnotate the element you moved to
EscClose the popup, or drop the current selection

There are deliberately no single-letter shortcuts. Tack runs on top of your application, and your application's shortcuts should win.

JavaScript API

window.__tack exists as soon as the script has loaded. This is how an agent with browser control reviews a page it does not own: load the URL, inject tack.js, annotate, then read md() or hand back link().

__tack.on()Activate without adding #tack to the URL.
__tack.off()Deactivate and remove every trace from the page.
__tack.add(el, note)Annotate an element or CSS selector. Returns the note.
__tack.add(el, "", {to})Propose exact replacement text instead of a comment.
__tack.add(el, "", {a, to})Same, for an attribute: {a: "alt", to: "New alt"}.
__tack.add(el, note, null, {…})Propose styles: {"font-size": "24px", color: "#111"}.
__tack.open(el, {edit})Open the editor on an element for a human to fill in.
__tack.menu()Open the actions menu.
__tack.select([a, b])Stage a multi-element selection for the next note.
__tack.region({x,y,w,h}, note)Annotate an area rather than an element. Viewport coordinates.
__tack.list()All notes as plain objects.
__tack.md(all)Markdown export. false = this page, true = every page.
__tack.copy(all)Copy the export to the clipboard, then clear those notes.
__tack.link(all)Promise of a shareable URL carrying the whole review.
__tack.load(url)Import a review link. Resolves with the number of notes.
__tack.applied()Compare the last export against the page: [{note, status}].
__tack.prefs(obj)Read or set block, markers, freeze, light, open.
__tack.clear()Delete every note.
__tack.on()
__tack.add('#hero h1', 'Too vague — name the actual benefit')
__tack.add('#sub', '', { to: 'Ship your first widget in five minutes.' })
const review = __tack.md(false)
__tack.off()

Events

Tack reports what a reviewer did as plain DOM events on window, so a host page can measure the flow without the library ever making a request. Nothing listens by default, and if nothing listens, nothing happens.

tack:activate{notes} — Tack was switched on, with this many notes already on the page.
tack:note{here, total, edit, style, multi, region, source} — a note was saved. edit is true for a rewrite, style counts adjusted properties, region marks an area note, source is set when a file hint was found.
tack:export{notes, all} — Copy for AI handed the notes over and cleared them.
tack:download{notes, all} — the review was saved as a .md file.
tack:share{notes, chars} — a review link was produced. chars is its full length.
tack:verify{checked, applied, missing, skipped} — the applied check ran. skipped counts region notes, which have no expected value.
tack:import{notes} — a review link was opened and its notes loaded.
addEventListener('tack:export', e => {
  analytics.track('review handed over', { notes: e.detail.notes })
})

Payloads are counts and flags only — never note bodies, selectors or page text. That is deliberate: a listener will often forward these somewhere, and nothing a reviewer typed should be able to leave that way.

Privacy

The library sends no telemetry and never transmits your annotations. It makes no network requests of any kind — notes live in localStorage under tack_notes, and preferences under tack_prefs. A review link carries its contents inside the URL fragment, which browsers do not send to servers.

Loading the script from unpkg is itself a request to that CDN, as any script tag would be; self-host the file if that matters to you.

This website, gettack.dev, uses aggregate visitor analytics. See the privacy page.

Limits

← back to gettack.dev