The annotation language
GAL is a small language with twenty-five verbs. This page is about how they fit
together: which ones declare things, which ones make a claim about code, and
what has to be true before a finding appears or goes away. For the syntax of
each verb, the GAL reference is generated from
guardlink gal. In 2.2.0 that quick reference lists twenty-one of them: it
does not yet print the four reach verbs, @agents, @reaches, @effects and
@gates, which What an agent can reach covers
below.
Nouns are declared once, claims reference them
Section titled “Nouns are declared once, claims reference them”Every verb is one of two kinds.
Declarations name a thing and give it an #id. @asset, @threat,
@control, and @actor live together in .guardlink/definitions.js, and they
are the vocabulary the rest of the model draws on.
// @asset App.Users (#users)// @threat SQL_Injection (#sqli) [critical] cwe:CWE-89// @control Parameterized_Queries (#prepared-stmts)Claims say something about a specific place in the code, and they reference those ids rather than repeating their names.
@source file:src/users.js line:3 symbol:findUserByEmail@exposes #users to #sqli [critical] -- "email reaches the SQL string"The split is what makes the model checkable. A claim that references #sqli
either resolves to a declared threat or it does not, and guardlink validate
fails on the second case with a dangling-reference error. Names alone could not
do that: two spellings of “SQL injection” would silently be two threats.
The life of a finding
Section titled “The life of a finding”@exposes is the verb that creates work. Everything else either closes that
work, escalates it, or records a fact beside it.
flowchart TB
D["@asset, @threat, @control<br/>declared once, each with an #id"] --> E["@exposes<br/>opens a finding"]
E --> Q{"what is claimed next?"}
Q -->|"@mitigates"| M["closed, and a control is named"]
Q -->|"@accepts"| A["closed, and a person owns the decision"]
Q -->|"@confirmed"| C["still open, now verified exploitable"]
Q -->|"nothing"| O["still open, and ci --strict fails"]
class M,A emphasis
Only two verbs close a finding, and they close it for different reasons.
@mitigates says a control handles it and names which one. @accepts says a
person decided to carry the risk. Both remove it from the open list, and the
model keeps the distinction, so “we fixed it” and “we decided not to” never
collapse into the same silence.
The @exposes stays in both cases. Deleting it would delete the reason the
control exists, and a later reader would have no way to tell a path that was
never risky from one that was made safe.
Everything else records context
Section titled “Everything else records context”The remaining verbs do not open or close anything. They attach facts that a reader, a report, or a downstream tool needs.
| What you are recording | Verbs |
|---|---|
| How data moves, and where trust changes | @flows, @boundary |
| What an embedded agent or another principal can invoke, what the code does, and who approves it | @agents, @reaches, @effects, @gates |
| What an asset holds and who answers for it | @handles, @owns, @audit |
| What you are taking on faith, and what checks a control | @assumes, @validates |
| Who is legitimately allowed to do what | @actor, @entitles |
| Responsibility moved elsewhere | @transfers |
| Grouping, notes, and sensitive code | @feature, @comment, @shield |
@boundary takes two forms. @boundary between A and B (also A and B, or
A | B) is an undirected pair. @boundary from <outer> to <inner> states which
side is less trusted, which matters when both sides are declared assets, the
usual case for an application and its database. validate fails a directed
boundary whose side resolves to no declared asset and no @flows endpoint.
@flows has a written definition in the specification: four endpoint forms,
chains such as A -> B -> C read as one record per hop, the optional via, and
route channels written via GET./orders/<id>. The same definition ships as test
data, guardlink/conformance/flows.json, so a reader of the model built
outside GuardLink can check itself against the parser.
@entitles is worth singling out. It answers “is this caller already allowed to
do this by design?”, and it deliberately has no power to suppress anything: it
never closes a finding, never gates testing, and does not appear in the SARIF
export at all. It only informs the recommendation a downstream tool makes. It
also cannot be written by hand or by an agent, because an over-grant would close
a real privilege escalation as by-design, so a proposal goes through
guardlink entitle and a human accepts it.
What an agent can reach
Section titled “What an agent can reach”An application that embeds an LLM agent hands it capabilities through its
harness: tool definitions, MCP servers, file and shell access, database
clients. @flows describes traffic the authors expect, not what a tool makes
possible. Four verbs declare that, each where its fact lives in the code:
| Verb | Written on | Says |
|---|---|---|
@agents <agent> to <capability> [on <asset>] [as <identity>] |
the tool registration | an LLM agent can invoke this capability. Writing it is what marks the actor as an agent |
@reaches <actor> to <capability> [on <asset>] [as <identity>] |
the credentials a job runs with | the same, for a principal that is not an LLM agent, such as a CI runner |
@effects <read|write|delete|execute|spend|notify> on <asset> [as <identity>] |
the code that acts | what that code does to the asset. The set is closed, like @handles |
@gates <asset> by <approver> [for <capability>] |
an approval step that waits | who decides before an effect lands. by is required |
/** * @agents #support-agent to run-sql on #tool-surface -- "Debug tool, still registered in production" * @effects write on #users-db -- "Runs model-written SQL verbatim via db.raw" * @exposes #users-db to #excessive-agency [high] cwe:CWE-862 -- "Reachable from the agent's tool surface, nothing approves the write" */tools.register('run_sql', ({ sql }) => db.raw(sql))
/** * @agents #support-agent to issue-refund on #tool-surface -- "Refunds a customer's order" * @effects spend on #payments -- "Moves money back to the card" */tools.register('issue_refund', (refund) => requireApproval(refund))
/** * @gates #payments by #support-human for issue-refund -- "requireApproval() parks the refund until a human approves it" */async function requireApproval(refund) { return queue.park(refund)}With #support-agent, #support-human and the three assets declared in
.guardlink/definitions.js, that validates clean. The capability is the same
token @entitles uses, which gives the model a
question it can answer: what can this agent do that nobody approved? A reach
that no cited @entitles covers is an unentitled reach:
npx guardlink lookup "unentitled reaches"{ "query": "unentitled reaches", "type": "unentitled_reaches", "count": 2, "results": [ { "actor": "#support-agent", "agent": true, "capability": "run_sql", "asset": "#tool-surface", "description": "Debug tool, still registered in production", "file": "src/tools.js", "line": 4, "claim_key": "b18c175e474ec153b2d3b16218c5c0bb3d6f34bb806376f47dba0ef83bd325e2:0", "near_misses": [] },Truncated after the first result; the second is issue_refund. --fail-on-found
makes the same command exit 1 when the list is not empty, which turns it into
a CI step. npx guardlink lookup "ungated effects" answers the other half: here
it lists the write on #users-db, because the only gate in the file covers
issue_refund on #payments.
None of the four verbs opens or closes a finding, and a gate suppresses
nothing. The model JSON carries both answers as reach_analysis;
guardlink report adds an Agents and LLM Reach section mapped to the OWASP Top
10 for LLM Applications; and the SARIF pentest profile lists every reach and
every mutating effect, entitled or gated or not, as a target to test.
Where the language stops
Section titled “Where the language stops”GAL describes claims about code, and it stops at the edge of the repository.
It has no verb for a deployment topology, a vendor relationship, or a process,
because none of those has a file:line to anchor to. A threat model that needs
those still needs prose somewhere, and guardlink does not pretend otherwise.
More importantly, none of these verbs is checked against what the code actually
does. @mitigates ... using #prepared-stmts is a statement that a person wrote
down. GuardLink parses it, resolves its references, and reports it. It never
reads the query to see whether the parameter is really bound.
Related
Section titled “Related”- GAL reference lists the verbs
guardlink galprints, with syntax and examples. - The specification defines every verb, including the reach verbs in §3.2.1.
- Install GuardLink writes the first
@exposesand closes it with a@mitigates. - The threat model as an artifact covers what these claims become once parsed.

