Captures your browser AI conversations into a local HipCortex substrate.
This repository is the perception layer only. It captures from browser AI surfaces (ChatGPT, Claude, Grok, Gemini, DeepSeek) and forwards normalized events to a local HipCortex runtime, which owns all memory and reasoning. It does not retain or reason over cognitive state itself, and it never discards an unacknowledged capture.
The retention boundary, the end-state goals, and their measurable acceptance criteria are specified in docs/END-STATE.md. Read that before changing behavior here. The verified network contract — every endpoint that was actually executed against a live runtime, including the ones that must never be used — is in docs/PROTOCOL.md. Do not add a network call that document does not list.
The implementation plan is the change set under
openspec/changes/.
cortexbridge-perception-layer is the base
change; cortexbridge-retention-boundary, cross-provider-search-index, clarity-protocol,
substrate-migration, capture-context-injection and installable-product discharge what it
deferred. Every task in a tasks.md names the G#.# acceptance criterion it discharges and the
test that proves it.
It is built so that every claim about it is provable by a command: see Development.
| Component | Version | Declared in |
|---|---|---|
| This extension | 0.2.3 |
package.json, public/manifest.json |
| HipCortex core it was measured against | 3.11.0 |
every observation in docs/PROTOCOL.md |
Re-measured 2026-09-20 rather than carried forward. The runtime answers its own version, and that reply is what the second row names:
GET /health : HTTP 200 {"service":"hipcortex","status":"ok","version":"3.11.0"}
The two rows are deliberately independent, and the reason is worth stating because it looks like
drift otherwise. The extension pins no core version. It reads the runtime's version from
GET /health and reports it on the popup's health indicator; it does not gate on it, refuse a
capture because of it, or carry a copy of it (G7 — a pre-authorised remote host is the thing this
repository refuses, and a version allowlist is a different mechanism with the same failure mode:
the extension deciding, from a stale constant, that the user's own core is unacceptable).
So a core later than 3.11.0 is not refused — it is unmeasured. Every claim in
docs/PROTOCOL.md about what the runtime answers is a dated observation of
3.11.0, and re-running the probes against a newer core is a task, not an assumption.
One asymmetry is recorded here because it looks like drift and is not. The HipCortex source tree on
this machine declares 3.16.0 in its Cargo.toml, while the binary actually running answered
3.11.0 when these observations were taken and 3.12.0 during the session of 2026-09-23 — the
source has moved ahead of the built server. A 3.16.0 server has therefore not been measured by
anything in this repository, and the version row above keeps saying 3.11.0 for that reason. This
repository is the perception layer and never builds or ships the core, so that is an observation
about the machine, not a task in this tree.
The published channels are a different matter, and they do not agree with each other. Re-measured on 2026-09-23:
| Core surface | Says | Verdict |
|---|---|---|
Cargo.toml, VERSION, sdk/python/pyproject.toml, vscode-extension/package.json |
3.16.0 |
agree with each other |
PyPI hipcortex |
3.16.0, uploaded 2026-09-22 |
the wheel is current |
| PyPI long description | "HipCortex Python SDK (v3.12.0)", and serverInfo = 3.12.0 |
stale in the description, four minors behind the wheel that carries it |
npm hipcortex |
0.5.2, published 2026-07-20 |
stale by two months, and on a line of its own |
sdk/typescript/package.json |
3.12.0 |
four minors behind Cargo.toml |
| the binary answering on port 3030 | 3.11.0, later 3.12.0 |
behind the source, and intermittently unreachable |
An earlier reading of this manifest concluded that a published channel was mislabelled; that conclusion was wrong and stays withdrawn. The table above is a different fact rather than a revival of it: the versions are where the authors put them, and they are simply not equal.
The npm channel is not merely late. The core's Publish npm package workflow builds a valid tarball
on every release and then stops at npm error code ENEEDAUTH, because the registry token it
publishes with is not present — forty failed runs, back to 2026-05-20, which is as far back as the
API returns. That is a defect in the core repository and it cannot be repaired from here: the fix is
a registry token, and a registry token is a secret this repository must never hold. That package is
stale in its description as well as its version — its own README still advertises a 0.5.x server
matrix and a 0.5.7 VS Code extension.
What is stale on this machine is the install, not the label. The listener on port 3030 has been up
since before the current core was installed, so it keeps answering the version it was started with.
Replacing that payload and restarting that process is what would make the reply read 3.16.0;
nothing in this repository does either. A perception layer that rewrites and restarts the user's
runtime to tidy its own health readout has the boundary backwards. That listener was also
intermittently unreachable during the session of 2026-09-23: two independent clients on this machine,
fetch and PowerShell, failed and succeeded in the same rounds, so the flakiness belongs to the
core rather than to either client. It was reported and it was not touched.
From source. npm install, npm run build, then chrome://extensions → Developer mode →
Load unpacked → select dist/. This is the only install path that works today.
Chrome Web Store — not yet listed. The submission archive is built and attached to the
v0.2.3release ashipcortex-chrome-extension-v0.2.3.zip. The listing's short description is the manifest'sdescription, and it reads:Stop losing your AI conversations. Keep ChatGPT, Claude, Grok, Gemini and DeepSeek chats in a private memory on your own machine.
That is 129 characters against the store's 132-character limit, and it is the field the dashboard makes read-only after the first upload — so it was settled before submission rather than after.
0.2.2is the release that rewrote it: the sentence it replaced said own twice and named the mechanism instead of the result, and private is the word that separates this from every other tool that also reads a ChatGPT conversation.0.2.3leaves that sentence byte-identical — it is the release that carries the capture and native-host fixes, and a field the dashboard locks is not the place to spend a version. The full listing copy is in docs/STORE.md §3.Uploading the archive is a manual step a maintainer performs; until that happens there is no listing to install from, and this section will not pretend otherwise. The ID the local build uses is not the ID the listing will carry: the store assigns its own item ID and refuses the
keyfield that pins the local one. §11 of that document is the step that reconciles the two, and skipping it breaks the native host for store-installed users.Everything the submission form asks for — the archive, the paste-ready listing copy, the per-permission justifications, the reviewer instructions, and an explicit list of what is still missing — is in docs/STORE.md. The privacy policy is docs/PRIVACY.md.
A draft of the item already exists in the dashboard — HipCortex Memory, status Draft, item ID
pjapbnibehgmgilenkgndfpfjkokikbl— and its Privacy tab is filled field by field from §4–§6 of that document, including the nine data-disclosure answers and the privacy-policy URL. That ID, and noteklnpdcephecmddelagbablmeajoogkf, is the one the published extension will carry; §11 is how the local build is made to answer to it.
Loaded from source, the extension ID is eklnpdcephecmddelagbablmeajoogkf, because
public/manifest.json pins a key. That is the ID of the unpacked build; the published item gets its
own ID, assigned by the store, which refuses a package containing the key at all — see
docs/STORE.md §11. On first run the extension reports which of the two transport
modes it is in and what that mode needs.
The account that will publish it is the Chrome Web Store publisher
742d17eb-82ee-46a9-9f3d-5cf0eee38065. A publisher ID is account-level — it identifies the
developer account that owns the item, not the item — so it has no manifest field, appears nowhere in
the package, and is not the extension ID above. Google support asks for it; nothing in this
repository consumes it.
Either way, Consumer Mode needs the native messaging host registered before capture can reach desktop. The extension cannot do that itself — no extension API can write a registry key — so it reports host not installed and names the command that fixes it. See Development.
The single statement that settles most design questions here:
The core retains; the extension guarantees acknowledged delivery.
The extension is not the retention boundary — and that is not permission to discard an unacknowledged capture. A queue entry is removed only on positive acknowledgement from the runtime (see docs/PROTOCOL.md §5). There is no loss counter, because there is no path that loses anything.
One thing the extension does keep, and this section would mislead without it: a local search
index, holding one bounded and truncated copy of each delivered conversation so that search
still answers while the runtime is stopped. It is a cache keyed to an acknowledgement rather than a
second retention boundary — nothing enters it before the runtime has confirmed delivery, the oldest
record is evicted at the bound, and the popup clears it in one action. 0.2.2 corrected both
docs/PRIVACY.md and docs/STORE.md, which until then claimed the extension kept
no copy at all; the store's single-purpose box is submitted to a reviewer against the permission list,
so that claim was the most expensive place to be wrong.
What that rules out from this repository, permanently:
- No consolidation, belief formation, goal tracking, causal lineage, embeddings or retention policy — those are the core's surfaces, reachable at most as pass-through UI.
- No remote host pre-authorised in the manifest, and no non-loopback base URL that can be saved without a confirmation naming the exact host.
- No typed failure replaced by a plausible wrong capture.
- No conversation content in
chrome.storage.sync.
See docs/ARCHITECTURE.md for the flow and the layer rules.
- Capture this conversation: one button, on every surface, that captures the conversation in the current tab on demand — regardless of whether passive capture is on, because a click is not the extension acting on its own. It always answers: a capture that happened, a capture that is durably queued but not yet acknowledged, or a typed refusal naming the code and the reason. A button that appears to do nothing looks exactly like a broken extension, so there is no path through it that returns nothing
- Popup: health indicator, passive-capture switch, queue status, quick-add memory, search
- Side Panel: richer search + capture (Ctrl/Cmd+Shift+H)
- Context Menu: “Add selection / page to HipCortex”, “Search selection”
- Keyboard: Ctrl/Cmd+Shift+M = quick-add current selection
- Options page: base URL, API key, default actor, transport mode, feature flags, and the one-click site-access grant
- Migration: import a HipCortex export into the configured core — one record at a time through
POST /memory/add, neverPOST /memory/bulk, because bulk is measured losingtags,sourceandprioritywhile still reportingfailed: 0— and resolve an id from before the import through the recorded remap - Transport: Native Messaging (Consumer Mode) or verified localhost HTTP (Developer Mode)
- Local-first: default base URL
http://127.0.0.1:3030; no non-loopback host is pre-authorised inhost_permissions
| Mode | Transport | Requires |
|---|---|---|
| Consumer (default) | Native Messaging to the host com.hipcortex.bridge |
The HipCortex Desktop app, which registers the host |
| Developer | HTTP to a loopback base URL | A HipCortex server you started on http://127.0.0.1:3030 |
auto |
Consumer first, Developer as fallback | Either one |
Explicitly choosing consumer or developer means exactly that mode: consumer does not
silently fall back to HTTP, so picking it is a statement about where your captures are allowed to
go, not a preference. apiUrl is refused in auto and consumer modes if it is not loopback,
and the refusal names the host.
- Chrome / Chromium / Edge (Manifest V3).
- Consumer Mode: the HipCortex Desktop app, which registers the native messaging host
com.hipcortex.bridge. Nothing else — no separate server setup and no additional runtime. - Developer Mode: a HipCortex server reachable on
http://127.0.0.1:3030.
The popup reports passive capture on/off, queue length, the unacknowledged count and whether the queue is paused at its spill limit. A non-zero unacknowledged count is a normal transient state, not an error; a paused queue is a user-visible message naming the count, because the fix is to bring the runtime back rather than to wait.
Passive capture is on in a fresh profile, and the switch that turns it off is on the popup. The
setting decides whether the extension acts on its own when it notices a page change; a click does
not go through it. That interaction is the point: the extension cannot read a site until the user has
granted it that site, so "on by default" and "capturing something you did not ask for" are not the
same statement. Chrome's site access is granted from the Options page in one click, next to the
sentence it is about — the request has to be answered by the page that the gesture happened in, so
routing it through the worker would turn one click into a silent no-op — and the same sentence is
shown on the popup badge and on the first-run page, so the extension never captures while a screen
tells the user it is not reading anything. Revoking all six hosts is still one visit to
chrome://extensions.
npm install is required first: node_modules/ is not committed.
npm install
npm run build # tsc → dist/, then copies public/ assets including the icons
npm run watch # tsc --watch (does not re-copy assets)
npx tsc --noEmit # typecheck only
npm run lint # ESLint flat config over src/ and tests/
npm test # Vitest: a node project and a jsdom project
npm run verify # typecheck + lint + test + build — exactly these four, in this order
npm run test:traceability # every criterion in docs/END-STATE.md is cited, and every cited test path exists
npm run test:clarity # every question in docs/clarity-ledger.json has a state, and an exit where it needs one
npm run test:browser # loads dist/ unpacked into a real Chrome and drives the surfaces over CDP
npm run test:browser:providers # loads dist/ unpacked and observes a capture from five provider fixtures
npm run clean # removes dist/
npm run package # builds, then zips dist/ into hipcortex-chrome-extension-v<version>.zip
npm run install:host # registers the native messaging host for this user (no elevation)
npm run uninstall:host # removes it again, per userLoad unpacked: chrome://extensions → Developer mode → Load unpacked → select dist/.
npm run verify is the gate chain and nothing else is folded into it: the traceability, clarity and
browser commands are separate on purpose, because they read the plan documents or need a browser and a
built dist/, and a gate that fails for a reason unrelated to the code under change is a gate people
learn to skip. Run them when the change touches what they read.
Consumer Mode needs one more step, because a browser extension cannot install a native program
itself — no extension API can write a registry key, and permissions in public/manifest.json
contains neither downloads nor management, deliberately. Run npm run install:host after npm run build and reload the extension. uninstall:host reverses it; neither command needs administrator
rights. Without it the extension reports host not installed and names that command, rather than
telling you to check that something you have never installed is running. To check the install
actually took, run node scripts/probe-installed-host.mjs: it reads the registration back, spawns
the installed program the way Chrome does, and sends a side-effect-free health frame — it writes
nothing to your memory store.
An unpacked extension's ID is otherwise derived from its path, so it cannot be known before the first
load, which would make it impossible to register the host before loading the extension — the order the
install instructions above depend on. public/manifest.json therefore pins a key, and the ID of the
unpacked build is eklnpdcephecmddelagbablmeajoogkf. Because it is pinned,
chrome://extensions shows the same ID wherever the folder lives and across reloads, and the host's
allowed_origins is a value this repository determines rather than one it discovers.
That key must not be in the archive uploaded to the store, which rejects the whole package for
carrying it, and the store assigns the published item its own ID. The two are reconciled by replacing
the key with the store's public key after the first upload, which makes the unpacked build answer to
the store item's ID and keeps allowed_origins correct for store-installed users. The procedure, and
what breaks if it is skipped, is docs/STORE.md §11.
dist/ is built output and is not committed. Committing it used to let a stale build mask a
broken one, so npm run build before claiming a change works is a rule, not a suggestion.
npm run cleanandnpm run packageare cross-platform scripts, but only the Windows legs have been executed. The POSIX legs are unverified and must not be reported as passing until a POSIX machine or CI runs them.- Consumer Mode is installed and answering on this machine, browser leg unrun. The native
messaging host ships from this repository (
host/bridge-host.mjs) andnpm run install:hostregisters it; the framing, the envelope, the health probe and the per-user registration are executed by specs, and the registered host has been spawned and answered a health frame against a live core. What no spec here can run is Chrome itself resolving a registered host and handing the port to a loaded extension — that needs a real browser with the extension loaded. Nothing has been run against a HipCortex Desktop app either; this host is this repository's own. See docs/PROTOCOL.md §9. - Provider fixtures are structurally faithful but synthetic. They make the selector ladders regression-proof; they are not evidence that a rung matches today's live DOM.
- Every claim about a rendered browser surface is unrun by
npm run verify. That chain stops at jsdom, which boots the shipped HTML and the shipped controller and proves what the page sends and renders, not that Chrome paints it: the popup's online/offline indicator, the manual add, the context-menu capture, the side panel via Ctrl/Cmd+Shift+H, the options Save plus test connection, a live capture withautoCaptureon, and a real cross-provider context injection all need an unpacked load ofdist/. Two commands outside that chain do reach that load.- Those two are
npm run test:browserandnpm run test:browser:providers, and both are tracked commands rather than a private ritual, so a clone can re-run them. The first launches its own Chromium with--load-extension=distand drives it over the Chrome DevTools Protocol; like the second it has no dependencies beyond Node 22's globalfetchandWebSocket, and honoursHIPCORTEX_CHROMIUMif the browser is somewhere unusual. The second fulfils navigation fromtests/fixtures/so the shippeddist/content.jsexecutes against a real provider origin, and opens no socket to any provider at all. Re-run on2026-09-19against the0.2.2tree: the popup / options / side-panel pass reached 37 of 37 checks — including that a fresh profile reports passive capture on, and that clicking Capture this conversation on a page that is not a supported site answers with a typedPAGE_NOT_WATCHEDsentence and re-enables the button — and the capture pass reached 53 of 53, capturing five conversations withautoCaptureat its shipped default, reportingrefused=0andpaused=false, and writing nothing to the live core. Both aim their write paths at a stub or a dead loopback port, so a green run leaves the real memory store untouched.0.2.2ships the same compiled output as0.2.1— the two lines that reach the archive are manifest metadata — so these are the same numbers re-read on the rebuilt bundle rather than new ones.0.2.3is not that case. It changes compiled payload, so both passes above still describe the0.2.2bundle and have not been re-run against the bundle the version row near the top of this file names. Re-running them is a task, not an assumption, and until it is done those figures belong to0.2.2. - What neither command can reach is the gesture. The OS context-menu click and the physical Ctrl/Cmd+Shift+H keystroke are raised by browser chrome, above the renderer, where no synthetic input event lands. The handlers behind them are ordinary code and the specs drive them; the two clicks are a person's. That residue is the whole reason screen 8.12 is still unticked.
- Those two are
- The migration's machine leg is executed and recorded — one capture read back by REST export, the
hipcortex backupCLI and MCPsearch_memory, with the same messages in the same order — but the capture was produced by this repository's own egress code under Node, not by a real provider tab. That half is the same manual gate as above.
- Service worker (
background.ts) is the single message router and context-menu host. - Transport (
src/api/transport/*) is the only place allowed to touch the network. Capture egress isPOST /memory/add; the lossyPOST /memory/ingestis forbidden — see docs/PROTOCOL.md §3.1 for the evidence, which includes a 24-hour TTL that would delete the capture. - Providers (
src/capture/providers/*) hold all volatile DOM knowledge. Provider hostnames, selectors and ladders must not appear outsidesrc/capture/**, and no selector or landmark is ever fetched from the network or read fromchrome.storage. - Contract (
src/schema/*) is the versioned, provider-agnostic capture shape. It lives undersrc/rather than at the repository root because ofrootDir— see docs/PROTOCOL.md §10.1. - All UI surfaces talk only via
chrome.runtime.sendMessage. - Settings live in
chrome.storage.sync; conversation content never does. Ephemeral handoff such aspendingSearchuseschrome.storage.session.
The feature list above describes the end state. What is actually implemented and verified today is recorded per criterion in docs/END-STATE.md §Live verification. Anything not listed there as executed is not yet done.
The roadmap is the task list in the active OpenSpec change, not a separate document:
openspec/changes/cortexbridge-perception-layer/tasks.md— groups 1–10 deliver the perception layer.- Group 11 of that file records the follow-up changes:
cortexbridge-retention-boundary,cross-provider-search-index,substrate-migration,capture-context-injection, and the cross-repo dependencycore: add filter to POST /memory/search. cortexbridge-retention-boundary,cross-provider-search-index,clarity-protocolandsubstrate-migrationare fully ticked with their command output recorded. Three boxes stay unticked on purpose because they need a person at a browser or a real provider page, not because the work is missing:cortexbridge-perception-layer8.12 and 8.13, andcapture-context-injection4.3. The fourth,installable-product5.4, is a standing constraint rather than work — it is the instruction never to add a non-loopback host tohost_permissions, and ticking it would turn a rule into a dated claim that someone checked it.openspec/changes/installable-product/tasks.md— the native host and the per-user installer that make Consumer Mode installable, and the health surface that names which state the user is actually in (G9).
Apache-2.0 (aligned with HipCortex core) — the full text is in LICENSE.