● meerkat · knows
meerkat
A knowledge base served as a single static Go binary — three ways, CLI, MCP, and HTTP — so your developers and agents start from what is actually true at your org.
meerkat exposes a Markdown wiki as a CLI (mk search/show/list), an MCP server over stdio for agent harnesses, and an HTTP/OpenAPI server with bearer auth for tools like OpenWebUI. Content can be compiled into the binary at build time, or resolved at runtime from a directory or a signed archive. Either way search runs in-process with no external service and no network. It is the knows leg of the platform.
No credentials in the binary
The binary ships with no LLM credentials and is read-only over the knowledge base by default. There are exactly two "live" surfaces, both opt-in: ingestion shells out to your agent CLI under your own account, and a collection with a memory: block accepts saved memories over MCP.
Installation
The binary is meerkat, with a short alias mk. The repo is public and Apache-2.0, so installation works anonymously — no login or token required.
PLATFORM=darwin_arm64
mkdir -p ~/.local/bin
gh release download --repo zegit-zoo/meerkat \
-p "meerkat_*_${PLATFORM}.tar.gz" --output - \
| tar -xz -C ~/.local/bin meerkat
ln -sf meerkat ~/.local/bin/mk
meerkat versionNo tag is pinned above — gh release download with no tag argument fetches the latest release. Keep current with mk update --check then mk update. Releases are cosign-signed as a Sigstore bundle; mk update verifies the signature over the checksum file before trusting any hash, then does an atomic swap.
Container image
Every tagged release also publishes a multi-arch (amd64/arm64) OCI image to ghcr.io/zegit-zoo/meerkat — cosign-verifiable, with SBOM and provenance attached. The entrypoint is the meerkat binary itself, so it takes the same subcommands and flags as the CLI.
docker pull ghcr.io/zegit-zoo/meerkat:1.2.3 # pin the exact release
docker pull ghcr.io/zegit-zoo/meerkat:latest # newest tagged releaseImage tags drop the leading v
The git tag is vX.Y.Z but the image tags are X.Y.Z / X.Y / latest — pulling ghcr.io/zegit-zoo/meerkat:v1.2.3 fails with MANIFEST_UNKNOWN.
Core concepts
Content sources
The wiki body, the ingestion source registry, prompts, and templates are configuration, not code. The mechanism is content-source.yaml (copy from the example in the repo). The open-source build ships no content at all — every example returns nothing until you point it at your own.
content:
type: git # none | local | git | submodule | url | gcs
repo: your-org/meerkat-kb
host: github # github borrows a cached gh token; anything
# else uses your own git credentials
ref: v1.2.0 # tag or commit SHA (pin for reproducibility)
layout:
wiki: wiki # markdown pages
sources: ingestion/sources.yaml # the source registry
prompts: ingestion/prompts # per-source prompts
templates: templates # page templatesBuild time vs. runtime
Not every source type works at both ends. git and submodule are build-time only — they are resolved by make sync and baked in. local and url also resolve at runtime, so one released binary can serve different knowledge bases without a rebuild.
| Source | Build time | Runtime | Notes |
|---|---|---|---|
none | yes | yes | Embedded placeholders — the default. |
local | yes | yes | A directory on disk. Unverified. |
git | yes | — | Cloned at build; borrows a cached gh token for GitHub. |
submodule | yes | — | A git submodule in the repo. |
url | — | yes | An HTTPS .tar.gz. sha256 is required and verified before extraction. |
gcs | — | yes | A GCS .tar.gz object or bucket prefix. Application Default Credentials only (no key files); cached by object generation, optionally generation-pinned. |
A gcs source can additionally carry a refresh: block — a metadata-only probe on an interval that hot-reloads the collection when the bucket actually moved, with an atomic index swap and a serve-last-good failure policy. A pinned generation: and refresh: are mutually exclusive, refused at load time.
At startup meerkat resolves content in a fixed order, first match wins: --kb-dir (or MEERKAT_KB_DIR), then --content-source (or MEERKAT_CONTENT_SOURCE), then content-source.yaml in the user config dir, then one in the working directory, and finally the embedded build.
mk --kb-dir ~/kb list # serve a directory directly
mk --content-source ./content-source.yaml list
mk version --json | jq .kb_source # what am I actually serving?Provenance is explicit
mk version reports kb_source as embedded, disk:<path>, url:<url>@<digest>, or collections:<n> (with a per-collection breakdown in the JSON output). Only the url form is digest-verified — disk: means meerkat read whatever was on disk and checked nothing. The separate kb_commit field always describes the build-time embedded content and does not change to reflect runtime content.
Collections
Instead of a single content: source, a content-source.yaml can declare a collections: list — several named sources with heterogeneous backends (one local, one gcs, one url…), all mounted at once. Order matters: it is the order collections are listed, searched, and disambiguated in.
collections:
- name: runbooks
type: local
path: ../runbooks-kb
description: On-call runbooks — paging, escalation, recovery.
- name: handbook
type: gcs
bucket: my-org-knowledge
prefix: handbook/live/mk list --collections # what's mounted
mk search "incident" # every collection, merged by score
mk search "incident" --collection runbooks
mk show runbooks:incidents/paging # a collection-qualified page IDAn unqualified page ID is tried in collection order; an ambiguous one is an error naming the candidates. The first collection additionally backs the surfaces that aren't collection-aware yet: mk ingest and the ingestion source registry. Each collection can also declare a description: and an update: contribution contract (merge-request / direct / none) — surfaced to agents through collection discovery, so an agent learns what a knowledge base is for and how to contribute to it.
Memory: the one write path
A collection with a memory: block accepts saved memories over MCP: the mk_save_memory tool writes a structured Markdown document at personal, team, or global scope and indexes it immediately — no restart. The store sits outside the wiki tree (default a sibling memory/ directory; gcs backend for multi-replica deployments, with generation-preconditioned writes so replicas never lose an update).
- Without a
memory:block a collection is read-only and the tool is not offered at all — the pre-existing default. - Personal memories are private by default (
personal_visibility: private): readable only by the principal who saved them, and absent — not refused — for everyone else. - An unauthorized team/global write is staged under
_staging/for operator review rather than refused; there is deliberately nomk_delete_memory— removal is an auditable operator action.
OKF bundles
meerkat serves an OKF (Open Knowledge Format) knowledge bundle directly, with no conversion step — OKF is a directory of Markdown files with YAML frontmatter, which is already meerkat's storage model. meerkat implements the consumer side only: it reads bundles, it does not produce them.
A bundle's root is its content root, so point layout.wiki at it:
content:
type: local
path: /path/to/the-bundle # the bundle root itself
layout:
wiki: "." # concepts sit directly under itUse --content-source, not --kb-dir
--kb-dir always assumes the default layout (a wiki/ subdirectory) and has nowhere to carry a layout: override, so pointing it at a bundle root finds no pages and reports an empty knowledge base.
Two OKF frontmatter fields are promoted into meerkat's core: type (OKF's only required key, and a filter facet — see mk list --type) and description. The trust family — generated, verified, stale_after — is surfaced on every read path as two computed fields, trust_tier and stale. Everything else OKF defines (resource, sources, okf_version, the Attested Computation keys) is preserved verbatim under front.extra.
verified | trust_tier |
|---|---|
| absent | unverified |
present, no entry's by starts with human: | machine-confirmed |
present, at least one by starts with human: | human-reviewed |
trust_tier is advisory, not verified
The tier is asserted by whoever produced the bundle — meerkat derives it from the frontmatter but checks nothing. A producer can write verified: {by: human:anyone} freely. Read it the way you read the disk: provenance label, not as an integrity guarantee.
OKF reserves index.md and log.md for navigation, and meerkat skips them — but keyed on whether the file has frontmatter, not on the filename, so meerkat's own knowledge bases can keep using index.md as an ordinary landing page. OKF v0.2 is early and unratified, and meerkat is an independent third-party consumer, not affiliated with or endorsed by its authors. Full reference: docs/OKF.md.
Ingestion
meerkat is the planner; an agent CLI is the executor — --executor opencode (the default) or --executor claude. Pages begin as placeholders pointing at an upstream source; mk ingest renders one task per page, and mk ingest --execute spawns an agent session per page (under a wall-clock cap) to write the page, commit, and push.
mk ingest sources # list the source registry
mk ingest --source policies # plan only (writes a JSONL batch)
mk ingest --source policies --execute --max-parallel 4Search
An in-process Bleve BM25 index over the served markdown — no external service, no network, no embedding model. Built in-memory at startup; three indexed fields with boosts (title ×5, id ×3, body ×1). The body field accepts Bleve query strings.
Frontmatter filtering is post-search
Field filters like --owner and --type compose after the BM25 query — frontmatter is not indexed, so don't present owner:team-x as a working query term. Those filters exist on mk list, not on mk search.
Next
Authoritative reference, generated from the component repos. Spot something stale? Tell us.