Threat model
This page is for anyone who reviews the security of knowledge-trust-ladder, KTL Registrar and KTL Curator, or of a host that installs their skills.
The three repositories run the same scheduled agent from the same ktl-sidecar templates and release the same way, so they share this threat model. Each repository’s SECURITY.md says how to report, what that repository owns, and what it inherits from here.
Two facts shape everything below. The skills are prose executed by whichever large language model (LLM) agent runs them, so anywhere a skill sends the agent to read content it did not author is a prompt-injection surface. And the bundle’s central trust signal, a verified event whose actor starts with human:, is a string in a Markdown file that any writer can type.
Interactive use: scope is advisory, not enforced
Section titled “Interactive use: scope is advisory, not enforced”A skill’s Scope: line (“owns only .lokf/”) is prose, not a checked permission. The Agent Skills format has no manifest for that, and an agent run interactively has whatever tool access your harness grants it. npx skills prints the same warning after every install. Only the scheduled knowledge-librarian.yaml enforces its scope, because nothing is watching it run. Interactively, review what the agent changed before you commit or merge, as the scheduled path’s mandatory pull-request review does.
ktl-curator, ktl-librarian, ktl-prose, ktl-sidecar and ktl-website also run the repository’s scripts, from .lokf/scripts/. ktl-sidecar copies those files from its templates, and anyone who can commit to the repository can change them afterwards. A skill therefore runs them as the repository’s code, as just or make would, with whatever permission your harness gives it. ktl-website’s build also runs site.config.mjs, the site’s files under website/, and the commands site.config.mjs names, or a gen-doc recipe it finds, to build the schema’s pages and the data, and they are the repository’s code too. The preflight’s copies line cannot check them, because the preflight is one of them and a changed copy can print any line.
ktl-docent runs none of them, because a question about the repository is enough to start it. That repository is often one you have not reviewed, such as a fresh clone of someone else’s project or a pull request’s branch. The docent runs its own copies of knowledge-report.sh and knowledge-feedback.sh, from its scripts/ folder. A check in knowledge-trust-ladder holds each byte-identical to its template, and both read the repository as data. So a question runs no code the repository supplies. That holds only where the skill comes from a source you trust. A repository that holds its own copy of a skill, under .claude/skills/ for example, supplies the instructions and the scripts, and only your harness decides whether to load them.
Repository hardening
Section titled “Repository hardening”These are the measures the repositories take, and why. Each SECURITY.md says which of them its repository carries.
-
Actions pinned to commit SHAs in every workflow and in the seven workflow templates under
skills/ktl-sidecar/templates/github/andskills/ktl-website/templates/github/. Dependabot bumps the workflow pins. The templates are bumped by hand, since Dependabot scans only.github/workflows/. -
The skill a scheduled run installs is pinned to a commit. The librarian workflow names a release tag of this repository and the commit that tag named when the pin was set. A tag can be moved, and what it names is the instructions an agent follows unattended. So the install step installs nothing unless the tag still names that commit. The release step, the sidecar’s
knowledge-sync.shand itsknowledge-sync.yamlworkflow move the two together. -
A host’s sync runs no code from the release, and the branch it pushes carries no workflow the release wrote.
knowledge-sync.yaml’ssyncjob reads the release as data, undercontents: readand with no git credential on disk. Itspublishjob applies only the copies, the pin and the changelog, and holds the release’s workflow changes for a person. The sync workflow has the detail. -
The toolkit and the parser are installed as reviewed. Every workflow that installs the lokf toolkit does so with
uv sync --locked, so each file is checked against the hash.lokf/uv.lockrecords, and a lock that is missing or out of date fails the job. The Python halves of the conventions script and ofknowledge-apply.shname one PyYAML release, and take no file uploaded after a set date. Where there is no Python,ktl-librarianandktl-sidecarread LOKF’s schema from GitHub by commit, never by tag, so the file they read cannot change. Inknowledge-trust-ladderthe contract fails a tag or a branch in that URL, and two pages that pin different commits. -
permissions: {}at the top of every workflow, and harden-runner in audit mode on every job that installs packages or runs third-party code. -
The librarian runs in three jobs so the agent and the write token never meet.
earlierreads what became of the workflow’s earlier pull requests, with a token that can read pull requests and nothing else.refreshruns the agent withcontents: readand no git credential on disk, and hands its change on as a patch. Onlypublish, which runs no agent code, can write. What each job checks is under Prompt-injection guards. -
The librarian writes the bundle only through
knowledge-apply.sh, the pen. The agent’s one output is.lokf/patch.yaml, a list of operations. The pen checks each one and writes the files. It stampsgeneratedfrom the clock, refuses anyhuman:actor, and refuses to rewrite a concept a person wrote or to delete one a person confirmed or left a note on. It also compares each concept it is about to write with the file it read, and refuses the patch when a person’s event or note would differ, however the operations spelt it. It writes nothing unless every operation passes. -
Every secret is kept in an Environment limited to the default branch. A repository secret can be read by a workflow on any branch, and a workflow file runs on a push to the branch that carries it, with a
permissions:key that can raise its own token. So the librarian’s agent key, the sync token and the ingest credentials each belong in the Environment of the one workflow that reads them,knowledge-librarian,knowledge-syncorknowledge-ingest, limited to the default branch, and nowhere else. The templates name those Environments, and still read a repository secret of the same name, so a host works while it moves them. Every host should move them. A private repository on GitHub Free has no Environments, and its secrets stay readable from any branch. -
The agent step runs the reviewed, in-repo wrapper, never a repository variable’s content as a command.
AGENT_CLIchooses which agent,KNOWLEDGE_LIBRARIAN_ENABLEDturns it on, and the workflow triggers only onscheduleandworkflow_dispatch. The agent’s credential is theAGENT_API_KEYsecret or, withAGENT_USE_JOB_TOKEN, the job’s own token, which holdscontents: readandcopilot-requests: writeand expires with the job. The wrapper exports it only into the agent’s environment, under the nameAGENT_API_KEY_ENVgives, and refuses a name that is not a credential’s or thatgh, git or the runner also read. A domain host’s members script takes the job token asKNOWLEDGE_FEDERATE_TOKEN, removes it from its own environment, and hands it toghalone, to verify attestations. -
The release workflow packs the bundle and the write token uploads it, in separate jobs.
knowledge-release.yaml’spackjob installs the lokf toolkit and validates undercontents: read. Onlyattach, which runs no third-party packages, holdscontents: write, and it checks each zip, the bundle’s and any Copilot skill’s, against the checksumpackmade before uploading it. -
A domain host’s federation build reads the members’ releases and never runs them.
knowledge-federate.pyfetches only the files the member concepts name, over http or https from a public address. A local path needs--allow-local. A private address needs a person’sprivate_addresses: allowon the Domain concept, which a pull request’s check does not take as it stands. It checks each file against the checksum a person confirmed. For a GitHub release it checks the attestation too, signed by the member’s own release workflow, and stops withoutgh. It refuses a zip that holds a symbolic link, a path outside its root, a hidden file such as a.gitfolder, a file a browser runs, or more than a size limit unpacked. It also refuses a distribution name that is not one plain file name.Each connection to a member checks the address it opens, so a name that answers a public address to the check and a private one to the connection (DNS rebinding) reaches nothing. Through a proxy, the proxy resolves each name that the federation build and the ingest fetch, so only the check before the request applies, and the proxy must refuse a private address itself. Neither checks a name that resolves only at the proxy. The ingest’s git uses the proxy the ingest chose, or none, whatever git’s own configuration says. Every address that is not public counts as private, the shared address space 100.64.0.0/10 among them.
It runs the host’s report on each member with git pointed at no repository. A member of a member is confirmed by that member domain, not by the host’s person, so it brings content, not code.
In
knowledge-federation.yaml, thecheckandbuildjobs hold a read token, which reachesghalone. Onlypublishcan write. It checks the workspace zip against the checksumbuildwrote, which catches a fault in the transfer:builddecides whatpublishsigns. -
Every workflow write to
mainwaits for thereleaseGitHub Environment. Its required reviewers must be configured by hand, or the Environment exists in name only. Release tooling is installed withnpm install --ignore-scripts. -
mainblocks deletion and force-pushes and requires linear history, and no more. A rule requiring pull requests or passing checks would also reject the release job’s own push (how the repositories release). -
No tracked file holds a character a reader cannot see. A right-to-left override, the character the “Trojan Source” attack (CVE-2021-42574) hides code behind, renders as nothing in a diff, and so do a zero-width space and a byte order mark. GitHub warns of only some of them. In
knowledge-trust-ladderthe contract fails on any of them, andktl-prose’s check script reports one in any Markdown file it reads, in whichever repository installs it. -
Secret scanning and push protection are on. They are GitHub settings nothing in CI can assert, so keeping them on is a maintainer responsibility.
-
CodeQL and dependency review are off where there is nothing for them to scan.
The sync workflow
Section titled “The sync workflow”knowledge-sync.yaml keeps a host’s sidecar copies and skills pin at the newest release of this repository. No LLM, AI agent or AI tool runs in it, so a finding about third-party content reaching a model, such as Snyk’s W011, has no prompt to reach. A scanner, a person or a model reading the workflow may still raise four findings. Each is real, and each is answered by the workflow’s shape.
-
It fetches a repository at run time. The
syncjob clones the release without a checkout, before any person at the host has read it. Nothing from the clone runs. The host’s own copy ofknowledge-sync.sh, reviewed and merged there as every file the host runs is, reads the release’sknowledge-sync.manifest, the list of each template, its copy and the rule for it, and copies what the list names. It refuses a copy path outside the host’s tree or inside a.gitfolder, where a copy would be git’s own configuration, which can name a command git runs. It reports a rule it does not know instead of guessing, and git packages the change with no such command allowed.The job runs under
contents: read, with no git credential on disk and no secret in its environment, and only aftergit ls-remotefound the tag at the address the host’s own librarian workflow names. What leaves the job is a patch. The pull request’s own checks run the synced copies afterwards, as they run on any change to the host. -
A separate job applies what a sync may touch. The
publishjob reads the commit the tag names at this repository, then runs the default branch’s copy ofknowledge-sync.sh --apply. It refuses a patch that touches anything but the sidecar’s copies, the site’s copies, the pin and the changelog. It also refuses a path with a character outside the plain set the templates use, and a deletion, a rename, a link or an unusual mode. It keeps every workflow as the default branch has it and sets the pin itself. It writes the pull request’s body from the patch it applied, the default branch’s checklist and nothing thesyncjob wrote. -
A workflow on the pushed branch would run before review, with the repository’s secrets and a token it can raise, as Repository hardening says. So the branch carries no workflow the release wrote. The release’s workflow changes are held: the pull request shows their diff, and a person applies them after reading them, or approves them in a review Environment with required reviewers, after which a job pushes them. Before review, then, a release can change the scripts and site files that the pull request’s own checks run with a read-only token and no secret, and nothing else.
-
It holds a token that can write workflows.
GITHUB_TOKENmay not push a change under.github/workflows/, and every sync moves the pin there. So the host storesKNOWLEDGE_SYNC_TOKEN, a fine-grained token for that repository alone with Contents, Pull requests and Workflows set to read and write, in theknowledge-syncEnvironment limited to the default branch. It reaches thepublishandpush-workflowsjobs alone, and it does two things: push one branch,knowledge-sync, and open or edit one pull request. The jobs’ own token keepscontents: read. Without the secret the workflow attaches the patch to the run and opens nothing. -
It force-pushes. It does so only to
knowledge-sync, the workflow’s own branch, only over the workflow’s own commits there, and leased to the commit theearlierjob read, so a commit added since is never lost. A weekly run that finds a newer release while its pull request is open replaces those commits with a sync to the newer release, and sets the title and body to say so. The week’s releases are combined in one pull request.The
earlierjob first reads the committers on the branch, with or without a pull request, and a branch a person added a commit to is left alone. A committer’s name is a claim, so this check protects a person’s work from the force-push, and does not stop an attacker. A reviewer reads the diff as it is when they merge. A repository that gates merges on approvals should dismiss stale approvals when the branch changes.
A person reviews the pull request before the pin moves, as with every change the sidecar’s workflows propose. Run by hand, the script copies the workflows too, and runs the sidecar’s checks on the synced copies unless given --no-checks, so a person who syncs that way runs the release’s code on their own machine. A private host should set harden-runner’s egress policy to block in the sync job, and in the checks that run on the pull request, with this repository, GitHub and the package registries allowed, since the host’s checkout is readable there.
The website
Section titled “The website”knowledge-website.yaml builds the site from the repository and publishes it to GitHub Pages. Its build job runs npm’s packages, the lokf toolkit, just and the repository’s own code: site.config.mjs, the files under website/, and the commands that build the schema’s pages and the data. It holds a read-only token and no secret. npm ci --ignore-scripts runs no package’s install script, and Astro’s telemetry is off. Its deploy job runs harden-runner and GitHub’s deploy action, on the default branch alone, once the repository variable is set, in the github-pages Environment, which GitHub limits to the default branch. A pull request’s build runs that pull request’s code, as any check does, and its artifact can be downloaded by anyone who can read the repository.
What reaches the site is held to two rules. The sync copies a file only when it is inside the repository, and follows a link only when the link ends inside it. An SVG that holds a script, an event handler or a javascript: address is left out. Raw HTML in the Markdown passes as GitHub renders a README. A tag GitHub allows keeps the attributes it allows, and an address that is not http, https, mailto, a path or a fragment is dropped. Anything else is shown as text. A project site shares its origin with every other project site of its owner, so a script on one would run with all of them.
A private repository should set harden-runner’s egress policy to block in the build job, with the package registries allowed. BROWSE publishes its databases whole, and is off unless site.config.mjs turns it on.
The ingest workflow
Section titled “The ingest workflow”knowledge-ingest.yaml mirrors the sources .lokf/ingest.yaml names, from outside the repository, into .lokf/mirror/. No LLM, AI agent or AI tool runs in it. What it fetches reaches the scheduled librarian only after a person merges the pull request, so the mirror has the same standing as any other repository content: reviewed before an agent reads it. Four findings a reader may raise are answered by its shape.
- It fetches content at run time. It fetches only from the addresses the manifest names, in a job under
contents: readwith no git credential on disk. The script caps each body, in size and in time, each file it writes, and the mirror as a whole. It refuses an address that is not public unless the manifest allows it, at each connection as well as before the request, so DNS rebinding cannot reach one, except through a proxy, as Repository hardening says. It also refuses a content type the entry does not expect. It drops the credential on a redirect to another host, port or scheme, and removes every character a reader cannot see, apart from the joiners that Persian, Arabic and Indic scripts and emoji spell with. What leaves the job is a patch of Markdown files and the lock. - It runs git and the host’s own commands. A
gitentry names an https address, or http and a local path where the manifest allows private addresses. Git takes its protocol limits and the credential, scoped to that address, from its environment, which needs git 2.31. It follows no redirect with a credential, or where the manifest refuses private addresses. There, unless a proxy resolves the name, it connects only to the checked addresses, which needs git 2.37 for a host name. It refuses an address git’s configuration rewrites (url.<base>.insteadOf), which the check does not cover. Acommandentry runs only a command the host committed, with no shell and a clean environment. It runs as the script’s user, so the credentials it is not given are withheld as a courtesy, not by a boundary: a command is reviewed code, as any file in the repository is. - It holds secrets. The manifest names each credential as an environment variable starting
KNOWLEDGE_INGEST_, and the job hands over the repository secrets of those names and no other. Git gets no other credential: it runs with a home folder of its own, so curl reads no~/.netrc. The script prints no value, and the lock records none, apart from the last line a failing command printed, which is the command’s own. For a confidential entry, the committed lock records only how the run went, and no page’s title, address or author, unless the entry setslock_details: true. The job that holds the secrets has no write token, and the job that writes has no secret. - It force-pushes. It does so only to
knowledge-ingest, the workflow’s own branch, and only over its own single commit there. A branch a person committed to is left alone. Thepublishjob refuses a patch that touches anything but.lokf/mirror/and the lock.
The docent eval
Section titled “The docent eval”docent-eval.yml runs ktl-docent on a holdout of questions through claude plugin eval, weekly and by hand, never on a pull request or a push. An agent runs in it, so it has the scheduled librarian’s surfaces with less at stake. The job holds contents: read, keeps no git credential on disk, and writes nothing to the repository: the result goes to the run’s summary and a workflow artifact. Its API credential is the AGENT_API_KEY secret of the docent-eval Environment, limited to the default branch. The job checks the name the AGENT_API_KEY_ENV variable gives, as the librarian’s wrapper does, and exports the key under it, so the key reaches the eval’s process and nothing else. The job is inert until the repository variable KNOWLEDGE_DOCENT_EVAL_ENABLED is true, a cost ceiling stops it before a run starts once the ceiling is spent, and a pull request from a fork cannot start it.
The questions and the concept each expects live in tests/docent-holdout.yaml, outside .lokf/, so the docent never reads a question beside its answer. Each run starts in an empty directory into which evals/scaffold-bundle.sh copies the bundle alone: not tests/, evals/ or .lokf/feedback.md. The agent has read-only tools and no shell, as in any docent session, so a question runs no code the repository supplies. The scaffold runs as the operator, outside the agent’s sandbox: it is this repository’s code, reviewed as any other, and the job grants the agent no further tool. A concept written to steer an eval’s answer would steer the docent’s readers first, and the review before merge and the curator exist to catch it.
A green run proves that the docent named the expected concept, not that an answer was right. No judge reads the answers, so nothing in them reaches a second model.
Human attribution: human: is a claim, not a credential
Section titled “Human attribution: human: is a claim, not a credential”A verified event whose actor starts with human: says “a named person checked this concept against its source”. Everything downstream depends on it: ktl-curator’s health line, ktl-docent’s answer footers, and the OKF trust tiers. lokf validate accepts a forged one, because it is well-formed. The realistic threat is not an outside attacker but the ordinary shape of agent work: another agent driving ktl-curator and recording confirmations nobody gave. That agent may be an orchestrator, a subagent, a scheduled run, or a session where the “person” answering is really a tool result. An identity resolved from the local environment cannot tell that from a real session, because both arrive through the same channel.
Four measures answer it, in descending order of weight:
-
The forge is the authority. The forge is GitHub, GitLab or Forgejo. On each pull request, the
provenancejob inknowledge-registrar.yamlcollects the actor of everyhuman:event added, changed or removed under.lokf/knowledge/, whether averifiedentry or thegeneratedrecord. A removal is a claim too, since a person’s record is never removed without that person: averifiedevent struck out, or gone with its concept, needs the same evidence. A person’sgeneratedrecord may be replaced only by another person’s. The job reads the frontmatter whole, against every parent of a commit, keyed by the concept’sid: a re-dated event counts, and a renamed concept, a merge and an example in a body code fence do not.The job then requires evidence GitHub holds: an approving review from that account, or that account’s verified signature on the commit that introduced the event, since GitHub blocks approving your own pull request. This is the only measure a confused or adversarial writer cannot decline to follow, because it runs outside the agent. Where GitHub is not the forge, or as a second opinion where it is,
knowledge-provenance.shdoes the signature half with plain git and gpg or ssh-keygen. It checks against public keys the repository carries under.lokf/curators/, one GPG or SSH key per curator id. It refuses a change that adds an id’s key and that id’s confirmation together. -
ktl-curatorwriteshuman:only for an authenticated identity:gh api user,glab api user, or a signing key the forge lists under the login the person states. It never takes a typed name on its own, and never falls back togit config user.name, a value any writer can set, or to the “just ask” fallback, the one channel an attacker fully controls. Without an authenticated id, the two verbs that assert a person vouched for something are refused. The three that assert nothing about who checked what stay available. -
ktl-curatorrefuses to run a review session unattended: in CI, headless, as a subagent, on a schedule or a recurring prompt, or with answers arriving from a file or a tool result rather than a live turn. Its read-only report is safe to run anywhere. -
Detection, not only prevention. The curator’s report counts
human:events whose introducing commit carries no signature, so one that arrived by another route is visible to the next person who looks instead of silently trusted.
A confirmation covers the text the person saw. The label confirmed by a person holds while generated.at is no later than the confirmation. The pen restamps every change it writes, so a concept it edits reads edited since. Conventions rule 13 asks the same of every other writer: a pull request that changes a confirmed concept’s content after the commit that recorded the confirmation, and leaves generated behind, fails the validate job. Without the rule, a hand edit would keep the label, which would then vouch for text nobody confirmed. Rule 14 covers the commit that records the confirmation: on a pull request, such a commit may change what the concept says only when the person is also recorded as its author, as Correct now records them.
Limits. A signature proves a key holder made a commit, and an approval proves an account clicked a button. Neither proves anyone read the source. The gate raises forgery from “type four lines of YAML” to “control that person’s GitHub account or signing key”. It does not make a confirmation true. Where .lokf/ is gitignored, no pull request ever carries the bundle, so the CI half never runs and the curator’s in-session rules are the whole of it. A host without git has only its platform’s version history. The curator names that as the record, and the report says so every time.
The optional attestation job is off unless KNOWLEDGE_CURATION_ENVIRONMENT names an environment. It exists for repositories that cannot sign. It is not a switch that disables the check, since a permanent “provenance: off” setting, once used, tends to stay in place. It is a fresh, logged approval from an environment reviewer on each affected pull request. One misconfiguration matters: naming an environment whose required reviewers are empty makes it self-approve instantly, turning the gate into a false assurance. Configure the reviewers first.
Prompt-injection guards
Section titled “Prompt-injection guards”The six skills read content they did not author: repository files, external URLs, reader questions. They either act on it or hand it to a person. Each skill’s SKILL.md carries the guard for its input path, and this list names each one.
-
.lokf/feedback.md(ktl-librarian).ktl-docentwrites it from reader questions, and the scheduled librarian consumes it. It is the one input that can originate from someone with no repository access and reach an agent whose output is applied withcontents: write. The skill resolves only the question or disagreement an entry names, from the source it points at, never from the entry’s own wording, so an entry phrased as a directive is read as the content it reports.ktl-curatorreports only how many entries wait, counted withgrep -c, so their text never reaches its session.The librarian is the only skill that reads what a reader wrote. It keeps a reader’s words out of
log.md, out of every concept and out of its hand-off, since the curator opens the first two and a person reads the third. A handled entry leaves the file for.lokf/questions.md, a ledger the pen writes and only programs read. The pen takes every character a reader cannot see out of the question it files there, and refuses a patch that handles more than ten entries.knowledge-report.shcounts the ledger, and builds from it the one prompt of the retrieval test. An agent answers that prompt from an empty directory, and its reply is read for concept paths and nothing else. What the reply is scored against comes from the ledger alone, and a call that changed the checkout refuses the run. -
The librarian’s hand-off (
knowledge-librarian.yaml). The patch file may carry up to ten lines for the person reviewing the pull request, in the agent’s own words. It is the one free text that passes from the job that ran the agent to the pull request, and it is shown as the agent’s words and nothing more. The pen allows only printable text with no backtick in each line, and writes none of it to the bundle. It takes out every character a reader cannot see, as Unicode marks it, so it removes a bidirectional mark, a tag character or a variation selector as it removes a zero-width space.Once the agent returns, the wrapper removes whatever is at the paths the workflow reads, the hand-off’s and the retrieval score’s. So a file the agent left there is never read as the program’s, and a link it left there cannot send a later write into the checkout. The retrieval test calls the agent again, with readers’ words in its prompt, after the pen has written the hand-off. So the wrapper writes the pen’s lines there again once that call returns.
publishcleans the lines again and shows them under From the librarian, in a code block, where no link, image, mention or HTML renders. The curator never opens a pull request body, so even a reader’s words that reached the hand-off through the agent would not reach the one session that writeshuman:events. -
Fetched sources (
ktl-curator). The curator opens each concept’sresourceorsourcesand quotes it to a person before any verdict is recorded. Whatever a source contains is text to quote, never instructions. The verdict is always the person’s (“does the source still say this?”), never the agent’s own “looks consistent”. An adversarial source can be quoted at someone, but it cannot get itself accepted. -
Fetched sources and the repository (
ktl-docent). Verifying an exact value, or falling back once the bundle has no answer, means opening something the skill did not author: text to quote or summarise, never instructions. The docent is read-only on.lokf/knowledge/. Its one write path,.lokf/feedback.md, requires asking once per session first, never silently and never automatically. -
.lokf/feedback.mdon the way in (ktl-docent). The docent adds an entry with its copy ofknowledge-feedback.sh: it passes its entry as an argument and never opens the file, and the script prints only the kind, the date and a count. So no other reader’s report reaches the docent’s session, which also fetches URLs and reads repository files. The guard is the call, not a rule the agent has to keep. The script accepts onlyMissorDisagreementas the kind, only a forge login’s characters in the optional attribution, and only one line as the entry, so a caller cannot forge a second entry or an identity.A Disagreement may name its concept, which must be a concept file in the bundle’s lowercase spelling.
knowledge-report.shreads that path back to mark the concept disputed, and reads nothing else of the entry. The worst a caller can do with it is mark a concept disputed until the librarian runs, which changes no rung. -
The text being reworded (
ktl-prose). The body it rewords was written by an agent from repository content, so it is text to edit, never instructions. The skill never opens.lokf/feedback.md. It rewords only in a live session, on a request addressed to it. Its check script refuses a rewording that touches a concept a person wrote or confirmed, or a byte of frontmatter, so a sentence in a body cannot change who vouched for it. It also refuses one that adds a character no reader sees. -
Pull-request metadata (
knowledge-registrar.yaml). Theprovenancejob reads the pull request’s number, its base and head SHAs, and the reviews and commits GitHub reports for it, which a skill scanner flags as third-party content reaching an agent. No agent runs in that job. The event fields enter throughenv:, never interpolated. The API reads are narrowed to logins, SHAs and a verification flag. The one author-controlled string, thehuman:<id>in a concept’s frontmatter, is checked for a login’s characters only, before any fixed-string comparison. The token is read-only. The job does trust git’s listing of changed paths, so it lists them withcore.quotePathoff and refuses a path git still has to quote. -
Pull-request metadata (
knowledge-librarian.yaml). Theearlierjob lists the pull requests the workflow opened, so that a scheduled run opens no second one beside an open one and proposes nothing again that a person declined. A pull request counts only when its branch isknowledge-librarian/...in this repository, never a fork’s. From one closed without merging the job takes a number, a day, a base commit, the paths of the concepts it touched or added, and git’s hash of each feedback entry it handled. No agent runs in that job, and it reads no title, body or comment. Each value is matched against a pattern there and again byknowledge-report.sh, which shows the agent the paths and dates in its work list. The feedback entries are readers’ words, so only their hashes leave the job. -
The mirror (
ktl-librarian). A file under.lokf/mirror/is a copy of a page or document from outside the repository, written by the ingest and merged by a person before the librarian reads it. The librarian derives from it as from any source: text to derive from, never instructions. A Confluence page can be edited by anyone with access to the space. So a reviewer of the ingest pull request reads the diff as the page, and the manifest’s selector, a label the page’s owners apply, decides what is mirrored. -
If a guard fails. The only unattended write path is the
publishjob inknowledge-librarian.yaml, and the checks that count run there, on a clean checkout the agent never shared. Before it, inrefresh, the wrapper applies.lokf/patch.yamlwith the pen once the agent has finished, and refuses a run in which the agent changed anything else, so a direct edit of the bundle by the agent never reachespublish.Before
publishapplies therefreshjob’s patch, it re-derives the touched paths from the patch’s owngit apply --numstatand refuses anything outside.lokf/knowledge,knowledge_bundle,.lokf/feedback.mdand.lokf/questions.md. It separately refuses a patch that adds aby: human:line. It then checks the events on the patched tree withknowledge-provenance.sh --unattended. That refuses a person’s event added, changed or removed in any YAML layout, a person’s note added or removed, and a change to text a person wrote. The patch is built from those four pathspecs only.publishcomputes what the pull request says about the record withknowledge-report.sh, apart from the hand-off, which it shows as the agent’s words.The wrapper’s post-run check in
refreshonly stops a bad run early. An agent sharing that workspace could in principle defeat it by rewritingcore.fsmonitororcore.hooksPath, or by truncating the running script. Against the first, the wrapper snapshots.git/configand.git/hooksbefore the agent call and restores them from anEXITtrap, so they are restored even when the agent fails or the job is cancelled. Against the second, its checks are in amain()called last. The result is a pull request that never pushes tomainand never auto-merges. Merging it is a human decision like any other change.
What this does not cover. Ordinary repository content the librarian scrapes while building concepts, such as READMEs, docs and code, has no per-entry guard. It relies on the review-before-merge every change to main goes through, a higher trust level than unreviewed reader feedback. Nothing in KTL filters what a reader asks ktl-docent. That boundary belongs to the agent harness running the skill, not to a Markdown instruction file. Nor does any of this make a repository a sandbox. A compromised runner, upstream action or agent harness is out of reach, and a finding in one of those should still be reported to the affected repository, with scope and reproduction.
A skills repository is the case where that content is all instructions, since every SKILL.md is written for an agent to follow. The same guards hold there. The librarian reads each source as data, writes only patch.yaml through the pen, and holds contents: read in a scheduled run. An instruction in a source can still shape what a concept says. Only the person reviewing the pull request catches that, since the pen checks a patch’s form and never its truth. ktl-librarian’s references/agent-skills.md says which skills such a host derives concepts for.