Skip to content

Security

Archgate executes TypeScript rules from .rules.ts files in your repository. This page explains the trust model, what rules can and cannot do, and how to run checks safely.

.rules.ts files are executable code. When you run archgate check, the CLI dynamically imports every .rules.ts companion file and runs its check functions. This is equivalent to running bun .archgate/adrs/*.rules.ts — the code has the same capabilities as any other script on your machine.

This means:

  • Only run archgate check on repositories you trust.
  • Review .rules.ts files with the same scrutiny as any other source code in the project.
  • In open-source projects, treat .rules.ts changes in pull requests as security-sensitive.

Rules receive a RuleContext object with sandboxed file operations. All RuleContext methods (readFile, readJSON, grep, grepFiles, glob) are restricted to the project root directory — path traversal via ../, absolute paths, and symbolic links are blocked and throw an error.

In addition to the RuleContext path restrictions above, Archgate runs a static analysis security scanner on every .rules.ts file before executing it. Neither is a runtime sandbox — a rule still executes in-process with your privileges; the scanner is a static gate, and the restrictions apply only to the RuleContext API a well-behaved rule uses. Because a rule file runs in-process with the full privileges of whoever runs archgate check, any module it can reach is arbitrary code — so the scanner uses an allowlist, not a denylist: a rule may import only a small set of safe modules, and everything else is rejected.

The only modules a rule file may import are node:path, node:url, node:util, and node:crypto — utility modules with no filesystem, network, or process capabilities. They must use the node: prefix; the bare forms (path) can be shadowed by a package in the target project and are blocked. Every other import is rejected, in any form — static (import ... from), dynamic (await import(...), whether the specifier is a literal or a variable), re-exported (export ... from), or reached through require() / import.meta.require().

The scanner also blocks the other ways to reach code or capabilities outside that set:

PatternBlocked
Importing any module other than the four allowed aboveYes
require(), import.meta.require()Yes
Bun.spawn(), Bun.spawnSync(), Bun.write(), Bun.file(), Bun.$Yes
fetch()Yes
eval(), new Function()Yes
process.binding(), process.dlopen() (on any alias of process)Yes
Computed property access (Bun[variable], globalThis[variable])Yes
Assignment to globalThis or process.envYes
Bidirectional or invisible Unicode characters (“Trojan Source”)Yes

If any of these is found, the rule file is not imported or executed and archgate check exits with an error.

Because the scanner works from the parsed syntax tree, it sees through escapes: await import("\x6e...") resolves to the same module name the allowlist checks, so string tricks do not slip past it. The one thing a syntax tree cannot see is a character that makes the rendered source differ from the code that runs — which is why the scanner also rejects bidirectional and invisible Unicode characters outright.

Imported rules are scanned too. When you bring in third-party rules with archgate adr import, each .rules.ts is scanned — with a stricter check than first-party rules — before it is written to disk, so an imported pack cannot smuggle in code that your next archgate check would execute.

Well-behaved rules only use the RuleContext methods (ctx.readFile, ctx.grep, ctx.glob, ctx.ast, etc.) and ctx.report for output. When a rule needs language tooling — parsing Python, or comparing a file against its base git revision — ctx.ast() is the sanctioned door; a rule never needs, and cannot open, a subprocess of its own.

  • Write files — the RuleContext API is read-only. Rules report violations but cannot modify the codebase.
  • Escape the 30-second timeout — each rule is killed after 30 seconds of wall-clock time.
  • Affect other rules — rules from different ADRs run in parallel but share no mutable state through the context API.
  • Reach outside the allowlist through a recognized path — a rule may import only node:path, node:url, node:util, and node:crypto. Imports of any other module, Bun APIs (Bun.spawn, Bun.file), network access (fetch), subprocess or native-code access (require, process.binding), and code generation (eval, new Function) are rejected before execution in their direct, recognized forms. This is a static scan, not a jail: a capability assembled at runtime is a residual it cannot catch, which is why untrusted rule files still warrant review (see below).

Running archgate check in CI is safe when you control the repository content. Extra care is needed for pull requests from external contributors.

For pushes to main or other protected branches, archgate check runs code that has already been reviewed and merged. This is safe:

on:
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: archgate/check-action@v1

When a pull request comes from a fork, the .rules.ts files in the PR may contain arbitrary code. This is the same risk as running any untrusted CI script.

Option 1: Require approval before running. Use GitHub’s environment protection rules or pull_request_target with manual approval to gate CI on review:

on:
pull_request_target:
jobs:
check:
runs-on: ubuntu-latest
environment: pr-check # Requires manual approval
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: archgate/check-action@v1

Option 2: Only run checks on trusted files. Use a separate workflow that checks out the base branch’s .rules.ts files and runs them against the PR’s source files. This ensures only reviewed rules execute.

Option 3: Skip checks on fork PRs. If your rules are primarily for internal governance, skip automated checks on fork PRs and run them manually after review:

on:
pull_request:
jobs:
check:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: archgate/check-action@v1

Run archgate check on runners with minimal permissions. The job only needs read access to the repository — no secrets, deployment keys, or write permissions are required:

jobs:
check:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: archgate/check-action@v1

When cloning or forking a repository that uses Archgate, the security scanner automatically enforces the import allowlist in .rules.ts files. It is a strong first line of defense — not a complete sandbox — so you should still review rule files before running archgate check for the first time:

  • The scanner blocks the direct routes to dangerous capabilities, but it is static analysis, not a jail: anything built at runtime — a property name (obj[name] with a computed name) or a code string — is opaque to a source scan, so review rule files you do not trust before running them
  • Top-level code that runs on import (before the check function is called) is still executed if it passes the scanner
  • Well-behaved rules only use the RuleContext methods (ctx.readFile, ctx.grep, ctx.glob, ctx.ast, etc.) and ctx.report for output

The archgate login command stores your authentication token in the operating system’s credential manager (macOS Keychain, Windows Credential Manager, or Linux libsecret) via git credential approve. No credentials are written to disk as plain-text files. The token is used for plugin installation and is never sent to third parties beyond the Archgate plugins service.

  • Do not share the token in CI logs. Plugin installation commands pass credentials via authenticated URLs to git, which may appear in process listings. Avoid running archgate plugin install with verbose logging in shared CI environments.
  • To revoke access, run archgate login logout.

When you run archgate upgrade, the CLI downloads the release binary from GitHub Releases and verifies its SHA256 checksum before extraction. If the checksum does not match, the upgrade is aborted. This protects against tampered downloads due to network interception or compromised mirrors.

If you discover a security issue in Archgate, please report it responsibly by opening a GitHub issue or contacting the maintainers directly. Do not include exploit code in public issues.