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.0.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 annotated, or at least initialised.
link-projectinitialises a repository that is not set up yet.
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 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 unmitigated1 flows | 0 refs resolved | 2 unresolved⚠ 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)
The two counters at the end of the third 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.
--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.
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.
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.

