The Project Registry
The registry is Foundry's source of truth for which projects exist on your machine and what automation applies to each one. Without a populated registry, the daemon starts successfully but skips all project-specific work.
Where the Registry Lives
By default: ~/.foundry/registry.json
Override the path with the environment variable:
export FOUNDRY_REGISTRY_PATH=/path/to/my-registry.json
The daemon reads the registry on startup. If the file is missing it logs a warning and continues with an empty registry (no projects will be processed).
Single Source of Truth
The running daemon holds an in-memory copy of the registry in a read-write lock.
Online foundry registry list, show, add, edit, and remove commands all
go through the daemon's typed gRPC API so reads and writes observe the same
daemon-owned state. Direct file access is reserved for explicit recovery with
--offline. Without --offline, an unreachable daemon is an error and the
client-side registry file is left untouched. Online registry commands also do
not create FOUNDRY_REGISTRY_PATH when it does not already exist. The one
exception is foundry registry init, which is always an explicit offline-only
recovery command:
foundry registry add --name my-tool … # daemon-authoritative
foundry --offline registry add --name my-tool … # direct registry.json recovery
Registry Format (v2)
{
"version": 2,
"projects": [
{
"name": "my-tool",
"path": "/Users/alice/projects/my-tool",
"stack": "rust",
"agent": "claude",
"repo": "alice/my-tool",
"branch": "main",
"skip": false,
"actions": {
"iterate": true,
"maintain": true,
"push": true,
"audit": true,
"release": false
},
"install": {
"command": "cargo install --path ."
}
}
]
}
Top-level fields
| Field | Type | Description |
|---|---|---|
version | number | Must be 2 |
projects | array | List of ProjectEntry objects |
ProjectEntry fields
| Field | Required | Type | Description |
|---|---|---|---|
name | Yes | string | Unique human-readable identifier used in events and logs |
path | Yes | string | Absolute path to the project on your local filesystem |
stack | Yes | string | Technology stack — see Stack values |
agent | Yes | string | AI agent name used for automation (e.g. "claude") |
repo | Yes | string | GitHub repository slug (owner/repo) used by Watch Pipeline |
branch | Yes | string | Default branch (e.g. "main") — validation checks out this branch |
skip | No | string or null | Absent or null means not skipped; a non-empty string value is the skip reason. Accepts true (treated as "skipped") and false/null for backwards compatibility |
actions | No | object | Which automation steps are enabled; all default to false |
install | No | object | How to reinstall locally after automation — see InstallConfig |
installs_skill | No | bool or object | Whether to install the project's agent skill after the local install — see Skill install |
notes | No | string | Human-readable notes about the project (informational only) |
timeout_secs | No | number | Timeout in seconds for long-running commands. Defaults to 3600 (60 minutes) when absent |
update_policy | No | string | How far nightly maintenance may move dependencies: "patch", "minor" or "major". Absent means not set: maintenance behaves as "minor" and the maintenance summary flags the project. See Dependency update policy |
audit_exceptions | No | string[] (default []) | Legacy: advisory IDs accepted without an expiry. Kept for compatibility. Prefer the repository's .supply-chain-allow.json, which records a reason and an expiry in git. |
Accepted-risk advisories
Record an accepted advisory in the repository's .supply-chain-allow.json
(see Supply-chain scan).
Every entry has a written reason and an expiry, and it lives in git. The
post-push auditor, the scan_requested scan and the nightly supply-chain scan
all read it. An active entry accepts the finding; a lapsed one resurfaces it.
An entry matches when it names any identifier the scanner reports for the
advisory (for example PYSEC, CVE or GHSA for the same pip-audit finding).
The older registry field audit_exceptions still works, with the same
alias matching, but it has no expiry and no registry edit flag:
{
"name": "my-service",
"audit_exceptions": ["CVE-2026-45829"],
"notes": "CVE-2026-45829: chromadb vector store path is not exposed; accepted risk 2026-06-27."
}
Matching is case-insensitive. Each accepted finding is logged at info level and named in the audit's result line (accepted: PYSEC-2026-311 (allowlist)), so suppression is never silent. Remove an entry once the upstream advisory is patched.
Stack values
The stack field tells Foundry which audit tool to use and how to run
stack-specific commands.
| Value | Audit tool | Notes |
|---|---|---|
"rust" | cargo audit --json | Requires cargo-audit to be installed |
"typescript" | npm audit --json | Exit code 1 = vulnerabilities found (not a tool failure) |
"python" | .venv/bin/pip-audit --skip-editable --format=json | Runs the project's own pip-audit from .venv; a project without it is reported as not scanned. --skip-editable leaves out the project's own install, which is not a dependency. No JSON report (for example after a PyPI error) is a scan failure, never a clean scan |
"elixir" | mix deps.audit --format=json (mix_audit) in each Mix project | Foundry audits every Mix project in the repository that declares mix_audit as a dependency: the root mix.exs, or subprojects such as apps/* and vendor/* when there is no root project. deps, _build, node_modules and hidden directories are never searched. A repository where no Mix project declares mix_audit is reported as not scanned. Exit 1 means findings; output without a JSON report, from any subproject, fails the whole scan and names the subproject. mix hex.audit (retired packages) is not part of the audit: it is not an advisory database and has no JSON output; run it as a gate |
"cpp" | — | Placeholder for C++ projects; audit tooling not yet wired |
"swift" | osv-scanner scan source --format json --lockfile Package.resolved | Requires osv-scanner on the daemon's PATH and a committed Package.resolved. Exit code 1 = vulnerabilities found; 127/128 are tool failures. Findings carry the CVE alias when the advisory has one, otherwise the GHSA id |
"kotlin" | ./gradlew dependencyCheckAggregate --rerun --no-parallel --no-daemon | Runs the project's own OWASP Dependency-Check task, so its suppression file and settings apply. The project must configure formats to include JSON; Foundry reads build/reports/dependency-check/dependency-check-report.json (Dependency-Check 13 and later) or build/reports/dependency-check-report.json (earlier), whichever this run wrote most recently, and accepts a report only when this run wrote it. Gradle exits 1 both for a failBuildOnCVSS finding and for a broken build, so a missing or stale report is a scan failure. Findings follow the project's failBuildOnCVSS: a finding whose highest CVSS score is below it does not count (the project's build treats it as triage), suppressed findings never count, and without a numeric failBuildOnCVSS every live finding counts. --rerun stops Gradle skipping the task as UP-TO-DATE, which would leave the previous report in place. Timeout is 60 minutes because the NVD download is slow; set NVD_API_KEY in the daemon's environment |
Supply-chain auto-fix support differs by stack: Rust, TypeScript, and Python
have a full update and a targeted pin; Swift has the full update
(swift package update) only; Kotlin, Elixir, and C++ report no_fixer.
Dependency updates in the maintain workflow are agent-driven and do not
depend on the stack.
ActionFlags
The actions object controls which steps run during a maintenance run. All
flags default to false when the actions key is absent.
| Flag | What it enables |
|---|---|
iterate | Runs the iterate workflow (assess, plan, execute) after project validation passes |
maintain | Runs the maintain workflow — either after iterate completes (when both are enabled) or directly after validation (when only maintain is enabled) |
push | Pushes commits to the remote via git push after a commit is made |
audit | (Reserved for future use — currently informational only) |
release | (Reserved for future use — currently informational only) |
InstallConfig
The install field configures how the project is reinstalled locally after
automation completes. Exactly one variant is used per entry:
Command — runs an arbitrary shell command in the project directory:
"install": { "command": "cargo install --path ." }
Brew — installs via a Homebrew formula:
"install": { "brew": "my-formula" }
Homebrew installs run on macOS only. On any other host, Install Locally
skips the step with "install via brew not supported on brew; the maintenance summary shows the skip and its reason. Clear the
config on that host with foundry registry edit <name> --clear-install, or
give it a command install that works there.
An install command that fails, or cannot be started at all, is recorded as a
failed local_install_completed event with the reason in details, and the
maintenance summary lists it under Local Installs.
Skill install
The installs_skill field tells Foundry to run a skill-install command after a
successful local install. A failed skill install is a warning. It does not fail
the install.
| Value | Result |
|---|---|
absent or false | No skill install |
true | Run <binary> init --global --force. The binary is the brew formula for a brew install, and the project name for a command install. With no install config, Foundry does not guess a binary: it skips the skill install, and foundry registry show gives the reason |
{ "command": "..." } | Run this command exactly as written |
Use the object form when the binary name is not the project name (for example,
project hone-cli with binary hone). Set the field from the CLI with
--installs-skill true, --installs-skill false, or
--installs-skill "hone init --global --force".
Minimal Project Entry
Only the six required fields are needed. All optional fields default to safe values (no actions enabled, no install step, not skipped):
{
"version": 2,
"projects": [
{
"name": "minimal-project",
"path": "/Users/alice/projects/minimal-project",
"stack": "rust",
"agent": "claude",
"repo": "alice/minimal-project",
"branch": "main"
}
]
}
Excluding a Project Temporarily
Set skip to a string reason to pause automation without removing the entry:
{
"name": "on-hold",
"path": "/Users/alice/projects/on-hold",
"stack": "typescript",
"agent": "claude",
"repo": "alice/on-hold",
"branch": "main",
"skip": "Waiting for CI to stabilise"
}
The value of skip is the human-readable reason displayed in foundry registry show.
Absent, null, false, or an empty string all mean "not skipped".
For backwards compatibility, true is treated as the reason string "skipped".
The Validate Project block silently acknowledges skipped projects (emits
project_validation_completed with status: "skipped") so the engine trace
remains complete.
Managing the Registry with the CLI
Rather than editing registry.json by hand, use the foundry registry
subcommands. All commands respect FOUNDRY_REGISTRY_PATH for non-default
locations.
foundry registry list, show, add, edit, and remove are
daemon-authoritative in normal online use: they talk to foundryd over gRPC
and operate on the daemon-owned in-memory registry state. Direct filesystem
access is reserved for explicit offline recovery with --offline. Without
--offline, an unreachable daemon is an error and leaves the client-side
registry file untouched. If FOUNDRY_REGISTRY_PATH is absent, the online path
still leaves it absent. If daemon persistence fails during an online
add/edit/remove, the RPC returns a stable INTERNAL error and leaves the
daemon-owned registry unchanged.
# Create an empty registry during offline recovery
foundry --offline registry init
# List all projects from the daemon-owned registry
foundry registry list
# Inspect one project from the daemon-owned registry
foundry registry show my-tool
# Add a new project through the daemon
foundry registry add \
--name my-tool \
--path /Users/alice/projects/my-tool \
--stack rust \
--agent claude \
--repo alice/my-tool \
--iterate --maintain --push \
--install-command "cargo install --path ." \
--notes "Main CLI toolchain"
# Offline recovery: mutate the file directly while the daemon is stopped
foundry --offline registry add \
--name my-tool \
--path /Users/alice/projects/my-tool \
--stack rust \
--agent claude \
--repo alice/my-tool
# Edit an existing project
foundry registry edit my-tool --timeout-secs 3600
# Let maintenance take major upgrades (each one becomes its own task)
foundry registry edit my-tool --update-policy major
# Remove the install configuration
foundry registry edit my-tool --clear-install
# Run a skill install after the local install (true | false | "<command>")
foundry registry edit hone-cli --installs-skill "hone init --global --force"
# Skip a project temporarily
foundry registry edit my-tool --skip "Waiting for CI to stabilise"
# Clear a skip (resume automation)
foundry registry edit my-tool --skip ""
# Remove a project
foundry registry remove my-tool
See CLI Commands — foundry registry for the full option reference.
Multiple Projects
A single registry file can declare any number of projects. They are processed
concurrently during a maintenance run (up to max_concurrent at a time, which
defaults to the number of active projects unless the orchestrator is configured
otherwise):
{
"version": 2,
"projects": [
{
"name": "api-server",
"path": "/Users/alice/projects/api-server",
"stack": "rust",
"agent": "claude",
"repo": "alice/api-server",
"branch": "main",
"actions": { "iterate": true, "maintain": true, "push": true }
},
{
"name": "frontend",
"path": "/Users/alice/projects/frontend",
"stack": "typescript",
"agent": "claude",
"repo": "alice/frontend",
"branch": "main",
"actions": { "maintain": true, "push": true }
}
]
}