Link repositories into a workspace
A threat that crosses a service boundary belongs to neither repository on its own. The gateway knows it proxies a refund; the billing service knows it debits a ledger; only together do they describe the risk.
You will link two repositories into a workspace, produce a report from each, and merge them into one model. Everything here was run against GuardLink 2.2.0.
Before you start
Section titled “Before you start”- GuardLink installed. See Install GuardLink.
- Two repositories you can commit to. This page uses
gatewayandbilling. - Each one initialised with
guardlink init, and annotated.link-projectreports a repository with no prior setup asauto-initialized, but what it writes there is.guardlink/workspace.yamland agent instruction files: noconfig.jsonand nodefinitions.js. Runguardlink initin each repository as well.
How a workspace fits together
Section titled “How a workspace fits together”Each repository stays self-contained. It parses, validates, and gates on its own model exactly as before. The workspace is a second pass over the reports those repositories publish.
flowchart TB
G["gateway<br/>its own annotations"] --> GR["guardlink report --format json"]
B["billing<br/>its own annotations"] --> BR["guardlink report --format json"]
GR --> M(["guardlink merge"])
BR --> M
M --> W["one model across both,<br/>plus a workspace dashboard"]
M --> U["cross-repo references<br/>it could not resolve"]
class W,U emphasis
Nothing about the per-repo loop changes. That matters for adoption: a service team keeps its own gate, and the workspace is assembled from what they already publish rather than from a central checkout of everything.
Link the repositories
Section titled “Link the repositories”-
Point
link-projectat the repositories and name the workspace.Terminal window npx guardlink link-project gateway billing -w paymentsLinking 2 repos into workspace "payments"...⚡ gateway — auto-initialized (no prior guardlink setup)⚡ billing — auto-initialized (no prior guardlink setup)✓ gateway — workspace.yaml written, agent files updated✓ billing — workspace.yaml written, agent files updated✓ Linked 2 repo(s) into "payments"↻ Updated 4 agent instruction file(s)Next steps:1. Review and commit .guardlink/workspace.yaml in each repo2. Add "guardlink report --format json -o guardlink-report.json" to each repo's CI3. Use "guardlink merge" to combine reports into a unified dashboard -
Read what it wrote. Each repository gets its own copy, naming itself and its siblings:
gateway/.guardlink/workspace.yaml workspace: paymentsthis_repo: gatewayrepos:- name: gateway- name: billingthis_repois what makes the file repository-specific, so the two copies are not identical and both are committed.
Reference an asset in another repository
Section titled “Reference an asset in another repository”A cross-repo reference is written #<repo>.<tag>, taking the repository name
from workspace.yaml. The gateway records that its edge flows into billing’s
ledger:
@source file:src/routes.js line:1 symbol:handleRefund@flows #edge -> #billing.ledger via POST./billing/refund@exposes #edge to #unauth-refund [critical] -- "No authorization check before proxying"Merge the reports
Section titled “Merge the reports”Each repository publishes a JSON report. In CI that is a step; here it is one command each.
-
Produce a report per repository.
Terminal window (cd gateway && npx guardlink report . --format json -o guardlink-report.json)(cd billing && npx guardlink report . --format json -o guardlink-report.json) -
Merge them.
Terminal window npx guardlink merge gateway/guardlink-report.json billing/guardlink-report.json -w payments --summary-onlyMerging 2 report(s)...✓ payments — 2/2 repos loaded7 annotations | 2 assets | 1 threats | 1 controls1 mitigations | 1 exposures | 1 unmitigated0 confirmed | 1 flows | 0 refs resolved | 2 unresolved0 unreadable annotation(s) (0 error(s))⚠ Tag "#billing.ledger" referenced in gateway but not defined in any repo (prefix suggests repo "billing" but no definition found)⚠ Tag "#gateway.unauth-refund" referenced in billing but not defined in any repo (prefix suggests repo "gateway" but no definition found)Truncated after the console summary.
--summary-onlythen prints the same figures as a markdown summary, with a table per section and a list of the unresolved references, and ends with the advisory line below.
The two counters at the end of the fourth line are the ones to watch.
refs resolved is how much of your cross-repo wiring joined up.
unresolved is how much did not, and the warning under it names the
repository the prefix pointed at, so you can tell a typo from a genuinely
missing definition. confirmed is counted separately from unmitigated because
an @accepts silences a reproduced exploit nowhere in the product, and the
unreadable annotation(s) line is the denominator for all of it: a repository
whose annotations did not parse is not a repository with nothing wrong.
--summary-only prints the text and skips the dashboard. Without it, merge
also writes workspace-dashboard.html, a self-contained page you can open
directly or publish from CI. --json <file> writes the merged model, and
--diff-against <file> compares against a previous merge for a weekly summary.
Advisory by default, and --strict exits 1 on an unmitigated exposure, a
reproduced @confirmed exploit, or a report it could not read:
Advisory — nothing here failed the build. Run with --strict to gate on it.Ask the merged model what is open
Section titled “Ask the merged model what is open”guardlink estate reads back the file merge --json wrote, so the estate answer
and the merge’s own totals come from one computation rather than two:
npx guardlink estate merged.jsonpayments — estate risk, from 2/2 repo report(s) merged 2026-10-08T06:29:33.643Z
Repositories ⚠ gateway — 1 open, 0 confirmed ✓ billing — 0 open, 0 confirmed
1 open exposure(s) — 1 critical
gateway [critical] #edge → #unauth-refund gateway/src/routes.js:1 No authorization check before proxyingTruncated after the open list. The output continues with the merge’s warnings
under From the merge:, a note on tag ownership, and the advisory line.
It says how much of the estate it read first, because “0 open” over 0 of 4
repositories is the same vacuous green one level up — an unexpanded glob and a
pipeline that never uploaded both reach it. --strict exits 1 on an estate
that read nothing. It also refuses a per-repo threat-model.json: both are
model-shaped JSON, and a lenient reader would answer “0 repos, 0 open” from a
file describing one repository.
Duplicate definitions get an owner
Section titled “Duplicate definitions get an owner”Two repositories that each declare the same tag are not an error. The merge picks one as authoritative and says which:
⚠️ Tag "inj" defined in svc-a (owner) and also in: svc-bWorth reading rather than ignoring. Two services that both define #inj usually
mean two teams described the same threat separately, and the merged model will
carry one description for both.
Wire it into CI
Section titled “Wire it into CI”The pattern the tool suggests, and the one the counters are built for:
# in each service repository, on merge to the default branchnpx guardlink validate .npx guardlink report . --format json -o guardlink-report.json # publish this artifact
# in the workspace repository, on a schedulenpx guardlink merge reports/*.json -w payments \ --json merged.json --diff-against last-week.jsonKeep each service’s own gate as it is. The workspace pass answers a different question, and a service team should not have its build broken by a sibling’s annotations.
Both halves ship as workflow files you can copy rather than transcribe:
examples/ci/per-repo-report.yml
goes in each service repository, and
examples/ci/workspace-merge.yml
goes in the one that publishes the dashboard.
Their README
covers the personal access token the merge job needs to read artifacts across
repositories.
Related
Section titled “Related”- Annotate an existing codebase builds the per-repo model this assembles.
- The threat model as an artifact
covers the report schema and what
mergewarns about across versions. guardlink link-projectandguardlink mergedocument every flag.

