Skip to content

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.

  • GuardLink installed. See Install GuardLink.
  • Two repositories you can commit to. This page uses gateway and billing.
  • Each one annotated, or at least initialised. link-project initialises a repository that is not set up yet.

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.

  1. Point link-project at 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 repo
    2. Add "guardlink report --format json -o guardlink-report.json" to each repo's CI
    3. Use "guardlink merge" to combine reports into a unified dashboard
  2. Read what it wrote. Each repository gets its own copy, naming itself and its siblings:

    gateway/.guardlink/workspace.yaml
    workspace: payments
    this_repo: gateway
    repos:
    - name: gateway
    - name: billing

    this_repo is what makes the file repository-specific, so the two copies are not identical and both are committed.

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:

gateway/.guardlink/annotations/src/routes.js.gal
@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"

Each repository publishes a JSON report. In CI that is a step; here it is one command each.

  1. 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)
  2. Merge them.

    Terminal window
    npx guardlink merge gateway/guardlink-report.json billing/guardlink-report.json -w payments --summary-only
    Merging 2 report(s)...
    ✓ payments — 2/2 repos loaded
    7 annotations | 2 assets | 1 threats | 1 controls
    1 mitigations | 1 exposures | 1 unmitigated
    1 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.

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-b

Worth 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.

The pattern the tool suggests, and the one the counters are built for:

Terminal window
# in each service repository, on merge to the default branch
npx guardlink validate .
npx guardlink report . --format json -o guardlink-report.json # publish this artifact
# in the workspace repository, on a schedule
npx guardlink merge reports/*.json -w payments \
--json merged.json --diff-against last-week.json

Keep 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.