No description
  • Rust 95.4%
  • Nix 4.6%
Find a file
tsk 0728aaa5bb
Some checks failed
CI / checks (push) Failing after 3s
fix(nix): provide a CA store to package tests
Tests that build reqwest/rustls clients panicked in the Nix build sandbox
with "No CA certificates were loaded from the system", failing nix flake
check and the CI job that runs it. Add cacert to nativeCheckInputs so its
setup hook exports SSL_CERT_FILE during the check phase.
2026-09-29 18:16:52 +00:00
.agents/skills/cashu init commit 2026-04-18 10:46:06 +01:00
.github/workflows chore(dev): pin toolchain and add reproducible checks 2026-09-03 08:27:15 +00:00
personas refactor(review)!: coordinate persona lanes and duplicate decisions 2026-09-06 16:43:19 +00:00
src fix(logging): stop logging raw provider error responses 2026-09-29 18:16:52 +00:00
tests Improve issue coverage investigation and diagnostics 2026-09-26 08:22:46 +00:00
.env.example feat(review): harden Goose integration and upgrade to v1.46 2026-09-03 08:27:14 +00:00
.gitignore feat(issues): add Jev triage and verified draft fix PRs 2026-09-22 12:07:36 +00:00
AGENTS.md chore: dep update 2026-09-17 14:53:29 +00:00
Cargo.lock refactor(jev): migrate decisions to the GDK provider API 2026-09-22 18:55:58 +00:00
Cargo.toml refactor(jev): migrate decisions to the GDK provider API 2026-09-22 18:55:58 +00:00
CODE_STYLE.md init commit 2026-04-18 10:46:06 +01:00
example.fiach.toml feat: add optional Jev deduplication and review lane selection 2026-09-18 18:58:05 +00:00
example.issues.toml Improve issue coverage investigation and diagnostics 2026-09-26 08:22:46 +00:00
flake.lock chore: dep update 2026-09-17 14:53:29 +00:00
flake.nix fix(nix): provide a CA store to package tests 2026-09-29 18:16:52 +00:00
ISSUES.md Improve issue coverage investigation and diagnostics 2026-09-26 08:22:46 +00:00
Justfile chore(dev): pin toolchain and add reproducible checks 2026-09-03 08:27:15 +00:00
LICENSE-APACHE init commit 2026-04-18 10:46:06 +01:00
LICENSE-MIT init commit 2026-04-18 10:46:06 +01:00
LICENSE.md init commit 2026-04-18 10:46:06 +01:00
README.md fix(logging): stop logging raw provider error responses 2026-09-29 18:16:52 +00:00
rust-toolchain.toml chore: dep update 2026-09-17 14:53:29 +00:00

Fiach

Main repository: ssh://git@git.cashu.dev/thesimplekid/fiach.git. The GitHub repository is just a mirror.

Fiach (Irish for Hunter or Seeker) is an autonomous, AI-powered PR reviewer built in Rust using the goose agent framework.

It acts as a background daemon that monitors configured GitHub repositories, checks out active Pull Requests, and uses an LLM (OpenRouter by default, or OpenAI, Anthropic, or Google directly) to review the code against a fully customizable Persona. If the agent finds actionable issues (like security vulnerabilities or code quality violations), fiach can automatically report them by commenting on the PR or opening a dedicated disclosure PR on a centralized tracking repository.


Issue automation

Fiach also supports project-area labeling, duplicate checks across issues and open PRs, and independently verified draft bug-fix PRs. See the issue workflow guide and example configuration. Nothing is merged or closed.

🚀 Features

  • Custom Personas: Define exactly what the agent should look for using a Markdown file. Use different personas for security audits, general PR code review, code quality checks, or architecture reviews.
  • Reporting Modes:
    • local (Default): Saves the generated report to disk.
    • pr-comment: Posts the report directly as a comment on the target PR.
    • sync-pr: Clones a designated disclosure repository (e.g., owner/security-audits) and opens a new Pull Request containing the findings.
  • Smart Daemon: Automatically polls for open PRs that have been active in the last 4 months (120 days).
  • Configuration File: Uses fiach.toml for easy setup of daemon, review, and additional repository contexts. Copy example.fiach.toml to fiach.toml to get started.
  • Skip PRs: Ability to skip specific PRs by number or repo#number format.
  • Mention Trigger: Set trigger_mention (or --trigger-mention) to a GitHub username so the daemon only reviews PRs where that account has been @-mentioned (e.g. commenting @fiach-bot), instead of every open PR. Each mention triggers one review — mention again to re-review. Only mentions from allowed_mention_users count (or, when unset, from commenters matching allowed_author_associations), and a trusted mention also lets the review run on PRs from authors outside the allowlist, so a maintainer can summon the bot on a new contributor's PR. A trusted mention also overrides the automatic backport exclusion; backports without a trusted mention remain excluded, and explicit skip_prs entries still take precedence. When the review starts, the triggering comment receives the review_start_reaction (default 👀) so the mentioner knows the bot saw it, and when it finishes with nothing actionable (no findings, all findings rejected by the verifier, or already reported) the 👀 is replaced with the no_findings_reaction (default 👍).
  • Author Allowlist: Restrict daemon reviews to trusted GitHub author associations before executing any reviewer workspace.
  • Worker Limit: Run multiple PR reviews concurrently with an optional cap for resource-constrained hosts.
  • Verifier Pass: Re-check actionable findings against the complete PR diff before any disclosure mode runs.
  • State Tracking: Uses a lightweight, embedded Rust database (redb) to remember which commit hashes have already been reviewed, preventing redundant LLM calls.
  • Workspace Isolation: Clones the repository and checks out the PR branch into a temporary directory before giving control to the AI agent, saving valuable context window and turns.
  • Per-Review Sandbox Logs: Sandboxed reviews write nspawn.log next to their report artifacts for easier debugging.
  • Interactive Web Server: The daemon includes a built-in HTTP server to monitor its status, view review history, and manually trigger reviews on-demand without waiting for the next polling cycle.

🛠 Prerequisites

  • Rust: 1.98.1 (or use the provided Nix flake: nix develop)
  • GitHub CLI (gh): Must be installed and authenticated (gh auth login).
  • Environment Variables:
    • OPENROUTER_API_KEY: For default OpenRouter LLM access.
    • OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY: Required only when using the matching direct provider.
    • TYPESAFE_API_KEY: Enables optional Jev lane selection and live deduplication. Without it, all configured lanes run and deduplication uses the coordinator fallback.
    • GITHUB_TOKEN: Host-only token for PR discovery and permitted disclosure actions.
    • FIACH_REVIEW_GITHUB_TOKEN: Separate read-only token used inside review sandboxes for cloning repositories and reading PR metadata.

Credential Scope

The review agent can execute shell commands. In sandbox mode, fiach forwards configured provider API keys and maps FIACH_REVIEW_GITHUB_TOKEN to GITHUB_TOKEN inside the systemd-nspawn container so the review can reach the selected LLM provider and read GitHub data. The host GITHUB_TOKEN is never forwarded to the model-controlled process.

Non-sandboxed reviews share the host process environment and do not provide this credential boundary. Use sandbox mode whenever the host token can perform write or disclosure actions.

The sandbox also bootstraps its own runtime environment for service deployments:

  • a CA bundle path for git and gh
  • writable Goose state and log directories
  • packaged domain skills for environments where the review workspace does not contain .agents/skills

Treat every credential forwarded into the review sandbox—the read-only review token and provider keys—as readable by the agent. Use least-privilege credentials:

  • Scope the host GITHUB_TOKEN only to the repositories and disclosure actions fiach needs.
  • Configure FIACH_REVIEW_GITHUB_TOKEN as a distinct fine-grained token with read-only repository contents, metadata, and pull-request access. Sandboxed reviews fail closed when it is absent; do not reuse the host disclosure token.
  • Avoid broad write access, org-wide scopes, or access to unrelated private repositories.
  • Use an OPENROUTER_API_KEY with the smallest practical billing and account exposure.
  • If using a direct provider, apply the same least-privilege and billing limits to OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_API_KEY.
  • Do not reuse high-trust personal credentials for the daemon.

You can copy the environment template to get started:

cp .env.example .env

Development Checks

The repository pins Rust 1.98.1 in rust-toolchain.toml; the Nix shell uses the same version. Run the fast contributor checks with:

nix develop --command just check

Run the package, NixOS module, Rust formatting, Nix formatting, and spelling checks exposed by the flake with:

nix flake check --no-write-lock-file -L

📖 Usage Examples

Provider request diagnostics

At the default info log level, provider request spans record the model, serialized payload bytes, and output-token limit actually sent. Review and issue stages attach repository and PR/issue context. Jev decision errors explicitly report requested_output_tokens="not_sent". These diagnostics omit request and response bodies, credentials, and raw provider error text. Payload bytes include tools and settings; they are not input token counts. Error categories distinguish explicit context/output limits and leave ambiguous token-limit errors unspecified.

Fiach treats HTTP 400/422 request rejections as terminal. PR reviews use the existing fatal-error policy (including stopping the daemon). Issue triage persists the rejection until its evidence/configuration fingerprint changes; transient failures retain timed backoff. Screening can still split oversized batches into smaller requests. Goose's own bounded transport retries remain separate.

Review cost limits

max_cost_usd (CLI: --max-cost, NixOS: maxCostUsd) limits observed spending across skill selection, finder lanes, verification, and duplicate suppression. With verification enabled, discovery can use 70% of this limit; the remaining 30% is reserved for verification. The verifier can also use any unspent discovery funds. Without verification, discovery can use the full limit.

Provider-reported charges take precedence over token prices, including explicit price overrides. OpenRouter returns the account charge in usage.cost, which Goose includes in recursive session accounting. When charges are absent, Goose estimates or configured/model token prices are used as a fallback. Logged costs can therefore include estimates and are not invoice reconciliation.

Limits are checked against observed usage; in-flight requests and parallel lanes can overshoot before their usage arrives. A reserve improves the chance of verification completing but cannot guarantee it. Budget-stopped reviews are saved as unverified, with the exhausted phase in the JSON and Markdown artifacts. They are reported as incomplete and do not trigger disclosure, even when notify_on_empty is enabled.

1. The Autonomous Security Daemon (Sync PR Mode)

This is the primary use case. Run fiach as a background daemon that monitors multiple repositories. Out of the box, the daemon searches for any open PR updated in the last 4 months.

When it finds an actionable, verifier-approved finding, it will push a report PR to a centralized repository (my-org/security-audits).

cargo run -- daemon \
  --repos "my-org/core-backend,my-org/frontend-app" \
  --report-mode sync-pr \
  --sync-repo "my-org/security-audits" \
  --interval 300 \
  --provider "openrouter" \
  --model "google/gemini-3.1-pro-preview" \
  --skip-prs "123,my-org/core-backend#456" \
  --allowed-author-associations "COLLABORATOR,CONTRIBUTOR,MEMBER,OWNER" \
  --max-workers 4

2. Single PR Review (PR Comment Mode)

If you just want to run a one-off review on a specific PR and have the bot comment its findings directly on that PR:

cargo run -- review \
  --repo "org/repo" \
  --pr 1835 \
  --report-mode pr-comment

3. General PR Code Review

For a non-security review focused on correctness, regressions, API compatibility, and maintainability:

cargo run -- review \
  --repo "org/repo" \
  --pr 1835 \
  --persona "builtin:pr-review" \
  --report-mode pr-comment

4. Local Only (Testing a New Persona)

Testing a new code-quality persona and just want to see the markdown output saved to your current directory:

cargo run -- review \
  --repo "my-org/repo" \
  --pr 42 \
  --persona "builtin:code-quality" \
  --report-mode local

5. Interacting with the Daemon Web Server

When running the daemon, an interactive web server starts automatically on port 3000 (configurable via --port). This allows you to inspect the daemon's history and trigger reviews on demand. Set FIACH_SERVER_TOKEN to require Authorization: Bearer <token> or X-Fiach-Token: <token> on review and job endpoints. Liveness, readiness, and aggregate metrics remain unauthenticated on the loopback-only listener.

  • Check health:
    curl http://localhost:3000/health
    
  • Check readiness: validates both the durable state store and the in-process scheduler.
    curl http://localhost:3000/ready
    
  • Read Prometheus metrics: exposes scheduler jobs, durable review status counts, and recorded review cost.
    curl http://localhost:3000/metrics
    
  • List all reviewed PRs:
    curl http://localhost:3000/reviews
    
  • Trigger a manual review immediately:
    curl -X POST -H "Content-Type: application/json" \
         -d '{"owner":"my-org", "repo":"repo", "pr":42}' \
         http://localhost:3000/review
    
    The accepted response is process-local job metadata:
    {"job_id":"<opaque-id>","status":"queued"}
    
    Manual and polled reviews share one FIFO scheduler. Its queue capacity is max(2 × max_workers, 16); manual work does not preempt a running review.
  • Inspect a queued or running job:
    curl http://localhost:3000/jobs/<job_id>
    
    Jobs report queued, running, completed, failed, skipped, or cancelled, along with their target and timestamps. The newest 1,000 terminal jobs are retained in memory. Job IDs and statuses do not survive a daemon restart; an unknown or evicted ID returns 404. Durable review claims left in_progress by a stopped daemon are marked failed during startup so normal retry policy can reclaim them immediately.
  • Get JSON metadata for a specific review:
    curl "http://localhost:3000/review?owner=my-org&repo=repo&pr=42"
    
  • Read the Markdown report for a specific review:
    curl "http://localhost:3000/review/content?owner=my-org&repo=repo&pr=42"
    
    Report content is loaded from the authoritative absolute artifact path stored with the review record, including reports produced by sandbox workers.

State database migration

The redb schema is versioned and migrated automatically when the daemon opens the database. Before a legacy database is migrated, Fiach writes a sibling <database>.v1.backup file. Legacy flat reports and sandbox run artifacts are recorded when they can be found; missing files leave the corresponding artifact path empty. Migration uses one database transaction, so a failed migration does not replace the original records.


📝 Crafting a Persona

Fiach uses a small coordinator prompt to manage review lanes and structured results. You can configure the daemon to use different personas via the --persona flag.

If omitted, it defaults to --persona builtin:security. You can also pass --persona builtin:pr-review, --persona builtin:code-quality, or an absolute path to a custom Markdown file.

To run multiple independent passes for each PR, configure personas:

[daemon]
personas = ["builtin:pr-review", "builtin:security"]
max_workers = 2

Each persona gets its own state key and report path, so a successful PR review pass does not suppress the security pass for the same PR/commit. In sync-pr mode, non-default personas also get persona-scoped sync branches and report files. max_workers applies to the expanded review-job queue: two personas across ten PRs creates twenty review jobs.

A custom persona file can contain these placeholders which are filled at runtime:

  • {repo} — The target repository.
  • {pr_number} — The PR being reviewed.
  • {base_branch} — The base branch the PR is merging into.
  • {report_path} — The absolute path where Fiach writes the final rendered Markdown report.
  • {skill_hint} — Instructions for loading optional domain skills.

Review Lanes

Every review runs the configured persona as a mandatory persona lane, even when review_lanes is empty. The parent uses a small coordinator prompt and delegates code inspection to that lane. Additional review lanes provide narrower lenses within the same review, sharing one report and verifier/disclosure flow. The persona lane name is reserved; configure its methodology through persona, not review_lane_prompts.

This differs from personas: multiple personas create separate review jobs, state keys, and reports; lanes feed back into the same parent finder, which submits one combined structured result.

Example for a general PR review:

[daemon]
persona = "builtin:pr-review"
review_lanes = ["correctness", "concurrency", "api-compat", "tests"]
max_review_lanes = 2

Example for a security review with a custom domain lane:

[daemon]
persona = "builtin:security"
review_lanes = ["input-boundaries", "cashu-mint", "state-atomicity"]
max_review_lanes = 2

[daemon.review_lane_prompts]
cashu-mint = """
Focus on Cashu mint correctness:
- quote idempotency
- blinded signature issuance
- keyset transitions
- accounting invariants
Only report concrete PR-introduced issues.
"""

Lane names and custom prompt keys are normalized before matching, so Cashu Mint, cashu_mint, and cashu-mint all refer to the same lane. If a lane has no custom prompt, Fiach uses a built-in focus prompt for known lanes such as security, correctness, concurrency, api-compat, tests, performance, and observability; unknown lanes get a generic focus prompt.

The built-in summary lane produces a neutral structured description of what the PR changes. It does not make a review decision and is stored separately from findings. Buzz delivery enables this lane automatically.

The coordinator launches Goose delegate subagents, collects all lane results, merges candidates describing the same root cause, and submits submit_finding or submit_no_findings. max_review_lanes includes the mandatory persona lane and any summary lane. Lanes return candidate JSON and do not call reporting tools; custom persona files keep their methodology and placeholders, but the appended lane execution contract overrides old instructions to submit findings or write reports.

You can optionally make a specialist lane conditional on the change being relevant:

services.fiach.reviewLaneConditions."public-wallet-api-ffi" = ''
  Run when the change could require corresponding updates to CDK wallet
  FFI bindings: public wallet APIs, signatures, behavior, shared exported
  types, errors, conversions, or the wallet bindings themselves.
  Consider shared types and re-exports. Skip only clearly unrelated changes.
'';

Keep the lane in reviewLanes; its existing reviewLanePrompts entry defines how it reviews code once selected. The equivalent TOML setting is [daemon.review_lane_conditions] or [review.review_lane_conditions], with lane names as keys and applicability conditions as string values.

Lane conditions are optional. An empty map preserves existing behavior. Lanes without conditions always run. Conditions must name configured lanes; empty conditions, conflicting normalized names, and conditions on the mandatory persona, summary, or pr-summary lanes are configuration errors.

With TYPESAFE_API_KEY available, Fiach evaluates conditions before delegation using the same diff base as the review, including incremental rereviews. Jev returns run, skip, or uncertain. Only skip with probability at least 0.98 and confidence at least 0.95 removes a lane. Missing credentials, uncertainty, API failures, insufficient budget, and empty, binary, oversized, or incomplete diff context all run the configured lanes. No PR title or description can authorize skipping a lane by itself. Conditions and diff content are passed as data to a tool-free classification request.

The selected lanes drive both delegation and completion tracking. Structured artifacts include lane_selection decisions with conditions, actions, model, probability, and confidence where available; Markdown distinguishes lanes selected to run from lanes skipped as irrelevant. A skip is never recorded as a completed review or a no-findings result. Selection has a thirty-second total timeout, uses the shared Jev request limits below, and counts against the finder budget. dedupe_existing_comments controls deduplication independently of lane selection.

After the independent verifier finishes, Fiach fetches current PR discussion and uses TypeSafe Jev to compare verified findings with existing comments. Set TYPESAFE_API_KEY in the process environment or NixOS service environment file; it is forwarded into review sandboxes. Jev decisions take effect immediately, without shadow mode. The integration uses GDK’s goose-providers decision API with the direct TypeSafe provider and pins the model to jev-1.13.0. Goose and its provider crate are pinned together to upstream commit 8a460f964ef5b165e35ab231e76ccfa0c133abbd, which includes decision-provider support. Fiach retains answer validation, caching, cost budgets, and shared provider cooldowns.

Jev compares each finding with batches of up to eight comments using typed choices: same root issue, different issue, or insufficient evidence. Suppression requires a same-issue probability of at least 0.98 and confidence of at least 0.95. A definite non-match requires a different-issue probability of at least 0.95 and confidence of at least 0.90 for every comment. Comment IDs come from the supplied discussion, and the artifact records the matched ID, model, probability, and confidence. These conservative thresholds are policy defaults, not an accuracy guarantee.

Uncertain comparisons, oversized inputs, missing credentials, and service failures fall back to the finder coordinator for the unresolved findings only. That continuation has only reporting tools. Valid Jev decisions survive fallback failures; any remaining unresolved findings stay eligible for publication. Both sandboxed and local execution finish deduplication before returning artifacts for host disclosure.

Lane selection captures the complete diff with three context lines per hunk, up to 80 KiB, plus up to 8 KiB of changed paths. Its serialized Jev request is capped at 96 KiB; deduplication requests retain a 24 KiB cap. These are byte limits, not token counts: Jev separately enforces its 32k-token state-plus-longest-question limit and 64k-token total request limit. API context-limit errors run all configured lanes. Diff-size failures and Git failures have distinct diagnostics, also recorded in lane-selection artifacts. Requests have a ten-second timeout and each Jev phase has a thirty-second total timeout. Inputs are never truncated to force a decision. Reported usage is included in review totals, using the pinned model's published price of $0.042 per million input tokens (output is free). Estimated request cost is checked against the remaining budget before sending. Usage from requests that fail without a usable response cannot be accounted for; Fiach does not retry those requests before falling back.

dedupe_existing_comments = false disables both Jev and coordinator discussion comparison. The fallback coordinator uses the finder provider/model. Verifier provider/model settings remain independent. timeout_mins applies separately to the initial coordinator/lane phase, verifier phase, and fallback duplicate-decision continuation. The sandbox hard limit allows those enabled phases plus five minutes for setup and the bounded Jev phases.

Buzz Review Threads

Fiach can publish completed reviews to Buzz through the embedded buzz-client and buzz-sdk libraries in the trusted host process. General review personas create a root message in the public channel containing the PR summary, followed by one direct thread reply for each verifier-approved, non-duplicate finding. Security personas create a separate thread in the configured private channel, and only when they have verified findings. Security content never falls back to the public channel.

[daemon]
personas = ["builtin:pr-review", "builtin:security"]

[daemon.buzz]
relay_url = "https://buzz.example.com"
public_channel = "00000000-0000-0000-0000-000000000001"
security_channel = "00000000-0000-0000-0000-000000000002"
private_key_env = "FIACH_BUZZ_PRIVATE_KEY"

Set FIACH_BUZZ_PRIVATE_KEY in the Fiach service environment and ensure that identity is a member of both channels. auth_tag_env may name an environment variable containing a NIP-OA auth tag when the identity is managed. Buzz delivery is additive to report_mode; a delivery failure is logged without changing the completed GitHub review result. Re-reviews of the same PR and persona reuse the original Buzz summary thread. Fiach stores the root event and delivered finding identities in its review database, then appends only new findings after later commits.

The daemon can also answer questions about a completed review when its Buzz identity is tagged inside the corresponding review thread:

[daemon.buzz.questions]
enabled = true
provider = "openrouter"             # Defaults to daemon.provider
model = "openai/gpt-5-mini"         # Defaults to daemon.model
allowed_pubkeys = []                # Empty allows any channel member
max_question_bytes = 4096
timeout_secs = 120

For example, reply in the thread with @Fiach why was this rated high severity?. Fiach requires the cryptographic Buzz p tag produced by a real mention; matching plain text is not sufficient. It uses the same private key and pubkey that publish summaries and findings, and replies beneath the question in the same thread while mentioning the asker.

Questions are matched to the persisted thread root and channel before any model request. Public threads receive only their public review artifact, while security-thread questions remain scoped to the configured private channel. The question model receives the persisted structured review evidence without shell, reporting, or GitHub disclosure tools. Processed event IDs and the relay cursor are stored in the review database so reconnects do not duplicate answers. The Buzz identity must have a profile/channel membership that lets clients resolve its display name into the correct pubkey.

For the NixOS module, enable the dedicated Buzz options:

services.fiach = {
  enable = true;
  personas = [ "builtin:pr-review" "builtin:security" ];

  buzz = {
    enable = true;
    relayUrl = "https://buzz.example.com";
    publicChannel = "00000000-0000-0000-0000-000000000001";
    securityChannel = "00000000-0000-0000-0000-000000000002";
    questions.enable = true;
  };
};

The module writes [daemon.buzz] and defaults Buzz-enabled services without an explicit personas list to the general PR-review and security personas. No Buzz CLI package is required. Put FIACH_BUZZ_PRIVATE_KEY (and the optional auth-tag variable) in services.fiach.environmentFile.

Structured Reporting and Verification

Fiach uses structured reporting as the default review path for all personas. The finder agent does not directly post to GitHub and does not decide disclosure from Markdown frontmatter. Instead, it submits candidates through in-process reporting tools:

  • submit_finding records one candidate finding.
  • submit_no_findings records that the finder found no candidates.

When candidate findings exist and verify_findings = true, Fiach runs a separate verifier session. The verifier may use the same provider/model as the finder, or a separate model configured with:

[daemon]
provider = "openrouter"
model = "google/gemini-3.1-pro-preview"
verifier_provider = "openrouter" # optional
verifier_model = "anthropic/claude-sonnet-4" # optional

The verifier reviews all candidates in one pass and submits one submit_verdict per finding. Structured verdicts are authoritative for metadata and disclosure. Markdown reports are still written for humans, alongside JSON artifacts:

  • report.md
  • report.structured.json
  • report.policy.json

Workspace preparation materializes at most 16 MiB of the aggregate PR patch in .pr_diff.txt, preventing oversized PRs from consuming unbounded host memory. When that cap is reached the artifact contains a truncation marker; the agent can inspect omitted changes one file at a time with safe_diff.sh.

For report_mode = "pr-comment", Fiach posts a GitHub PR review only when all host-side policy checks pass:

  • the PR is still open,
  • the verifier confirmed the finding,
  • the verifier marked it as introduced by the PR,
  • command transcript evidence is present,
  • the comment anchor is valid in the PR diff.

This is the same flow for builtin:security, builtin:pr-review, and builtin:code-quality: persona changes what the finder looks for, while the host-side reporting and disclosure policy stays deterministic. Invalid inline anchors are downgraded into the review summary. Merged or closed PRs never receive PR comments. report_mode = "sync-pr" can still publish the local rendered report to the configured sync repository.

See personas/security-persona.md for a complete example of an aggressive, CTF-style vulnerability hunting prompt.


❄️ NixOS Deployment

fiach comes with a NixOS module for easy deployment as a systemd background service.

In your flake.nix or configuration.nix:

{
  inputs.fiach.url = "github:your-org/fiach";

  outputs = { self, nixpkgs, fiach, ... }: {
    nixosConfigurations.my-server = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        fiach.nixosModules.default
        {
          services.fiach = {
            enable = true;
            
            # The repos to poll
            repos = [ "my-org/core-app" "my-org/website" ];
            
            # Polling settings
            interval = 300;
            filterByUpdated = true;
            updatedWithinDays = 120;
            prStates = [ "open" "merged" ];
            prLimit = 1000;
            maxWorkers = 1;
            verifyFindings = true;
            provider = "openrouter";
            
            # The persona to use (defaults to builtin:security if omitted)
            # Use builtin:pr-review for general non-security PR review.
            persona = "builtin:security";

            # Optional focused lanes inside the persona. These feed one combined report.
            reviewLanes = [ "input-boundaries" "cashu-mint" "state-atomicity" ];
            maxReviewLanes = 2;
            reviewLanePrompts.cashu-mint = ''
              Focus on Cashu mint correctness:
              - quote idempotency
              - blinded signature issuance
              - keyset transitions
              - accounting invariants
              Only report concrete PR-introduced issues.
            '';
            
            # Model to use
            model = "openrouter/anthropic/claude-3-7-sonnet";

            # Optional verifier override. Defaults to provider/model above when unset.
            verifierProvider = "openrouter";
            verifierModel = "anthropic/claude-sonnet-4";
            
            # Disclosure Configuration
            reportMode = "sync-pr";
            syncRepo = "my-org/security-audits";
            
            # Environment file containing host-only GITHUB_TOKEN,
            # read-only FIACH_REVIEW_GITHUB_TOKEN, and the selected provider API key.
            # FIACH_SERVER_TOKEN is optional and protects the local web control API.
            environmentFile = "/run/secrets/fiach-env";

            # Optional tracing filter. Use fiach=debug or fiach=trace when debugging.
            logFilter = "fiach=info,goose=warn,rmcp=warn,sacp=warn,reqwest=warn,hyper=warn,goose_providers::http_status=error";

            # Sandbox Isolation (Highly Recommended)
            # Isolates each PR review inside a systemd-nspawn container.
            sandbox = {
              enable = true;
              
              # Network Mode:
              # - "host" (default): Most reliable service-mode option today. Shares the host network stack.
              # - "bridge": Attach each sandbox to an existing br-nspawn bridge.
              # - "veth": Better namespace isolation. Allows outbound internet but blocks access to
              #   host-local services. This is namespace isolation, not egress filtering, and it does
              #   NOT restrict the sandbox to GitHub/OpenRouter only.
              # - "private": Fully offline.
              networkMode = "veth";
            };
          };
        }
      ];
    };
  };
}

Deploying optional Jev support

  1. Update your Fiach flake input to a revision containing Jev support. If the input is named fiach, run nix flake update fiach from your NixOS configuration repository after that revision is published to the branch you track. An older pin will not recognize reviewLaneConditions.

  2. Add TYPESAFE_API_KEY=your-key to the secret supplying services.fiach.environmentFile. Keep the key in your secret manager, not in a Nix string. For the Beekeeper setup with fiach-environment.service, add this line to the encrypted fiach-env.age source using your normal secret editing workflow. The existing assembly script copies it into /run/fiach/environment; no script change is needed. Fiach forwards the key into review sandboxes automatically.

  3. Add an applicability condition for each lane you want Jev to select. For the CDK configuration, add this alongside the existing lane prompts:

    services.fiach = {
      reviewLaneConditions."public-wallet-api-ffi" = ''
        Run when the change could require corresponding updates to CDK wallet
        FFI bindings: public wallet APIs, signatures, behavior, shared exported
        types, errors, conversions, or the wallet bindings themselves.
        Consider shared types and re-exports. Skip only clearly unrelated changes.
      '';
      dedupeExistingComments = true;
    };
    

    Keep public-wallet-api-ffi in reviewLanes and retain its full reviewLanePrompts entry. Leave security without a condition so it always runs. The key also enables Jev deduplication when dedupeExistingComments = true; there is no separate Jev enable switch. Finder and verifier provider/model settings stay as configured.

  4. Build and activate your NixOS configuration with your usual deployment command, for example sudo nixos-rebuild switch --flake .#my-server on the target host. Replace my-server with your configuration name. For the runtime credential assembly setup, refresh credentials before restarting Fiach:

    sudo systemctl restart fiach-environment.service
    sudo systemctl restart fiach.service
    sudo systemctl status fiach.service
    sudo journalctl -u fiach.service -f
    

    If your environment file is supplied directly by a secret manager, omit the fiach-environment.service command. Restarting Fiach is required for it to pick up a changed environment file.

  5. Inspect the next eligible review's JSON lane_selection field and Markdown Lane Selection section. A Jev decision records its model, probability, and confidence; a fallback records why the lane ran. A lane running does not by itself prove Jev was called. Existing reviewed commits are not automatically reviewed again just because the configuration changed. Deduplication is exercised when verified findings and existing discussion are available; matched decisions record the existing comment ID and Jev metadata. Decisions take effect immediately, without shadow mode.

To disable Jev entirely, remove TYPESAFE_API_KEY from the secret, refresh any assembled environment file, and restart Fiach. All configured lanes then run, and existing-comment deduplication uses the coordinator fallback. To disable only lane selection, remove reviewLaneConditions; Jev deduplication remains available. Setting dedupeExistingComments = false disables both Jev and coordinator deduplication, independently of lane selection.

Available Configuration Options

The following options are available under services.fiach:

Option Type Default Description
enable boolean false Enable the Fiach Daemon.
repos list of string none List of repositories to monitor (e.g., ["org/repo"]).
port integer 3000 Port for the interactive web server.
interval integer 300 Polling interval in seconds.
updatedWithinDays integer 120 Number of days to look back for updated PRs when filterByUpdated is enabled.
filterByUpdated boolean true Whether to include the updated:>= GitHub search filter when discovering PRs. Set to false to query only by PR state and draft setting.
prStates list of string ["open"] List of PR states to poll (e.g., ["open"], ["open", "merged"]).
prLimit integer 1000 Maximum number of PRs to fetch from GitHub per polling cycle.
skipPrs list of string [] PR numbers or repo-qualified PRs to skip.
allowedAuthorAssociations list of string ["COLLABORATOR", "CONTRIBUTOR", "MEMBER", "OWNER"] GitHub PR author associations allowed to trigger daemon reviews.
maxWorkers integer 1 Maximum number of review jobs to run concurrently per polling query. 0 means unlimited. With multiple personas, each PR/persona pair is a review job.
drafts boolean false Whether to include draft PRs.
provider string "openrouter" Goose provider to use, such as "openrouter", "anthropic", "openai", or "google".
model string "google/gemini-3.1-pro-preview" Model to use with the selected provider.
verifierProvider string or null null Provider to use for the verifier pass. Defaults to provider when unset.
verifierModel string or null null Model to use for the verifier pass. Defaults to model when unset.
environmentFile path none Path to an environment file containing host-only GITHUB_TOKEN, read-only FIACH_REVIEW_GITHUB_TOKEN when sandboxing is enabled, the selected provider API key, and optionally FIACH_SERVER_TOKEN.
logFilter string "fiach=info,goose=warn,rmcp=warn,sacp=warn,reqwest=warn,hyper=warn,goose_providers::http_status=error" Tracing filter passed to RUST_LOG for the daemon and sandboxed review children.
persona string "builtin:security" Single persona source to use (e.g., "builtin:security", "builtin:pr-review", "builtin:code-quality", or an absolute path).
personas list of string or null null Persona sources to run independently for each PR. Takes precedence over persona.
reviewLanes list of string [] Focused review lanes to run as Goose subagents inside each persona review before the parent finder submits one combined structured result.
reviewLanePrompts attrset of string {} Custom prompt text keyed by review lane name. Keys are normalized like reviewLanes before matching.
maxReviewLanes integer 3 Maximum number of review lane subagents to run concurrently inside each review.
withSkill string or null null Optional skill name to instruct the agent to use.
reportMode enum ("local", "pr-comment", "sync-pr", "hybrid") "local" Mode for reporting findings. hybrid comments on PR-introduced findings and syncs non-PR security findings.
syncRepo string or null null GitHub repository to sync reports to. Required if reportMode is "sync-pr", and for non-PR security findings in hybrid mode.
notifyOnEmpty boolean false Whether to create PRs or comments even if no findings were found.
verifyFindings boolean true Run a verifier pass before disclosure when findings are present.
timeoutMins integer 30 Timeout in minutes for each review session.
maxRetries integer 3 Maximum number of retries for LLM provider failures and failed review attempts.
retryDelaySecs integer 10 Initial delay in seconds before retrying an LLM failure.
maxCostUsd float or null null Maximum observed budget in USD for each review. Cancels finder, verifier, and duplicate-suppression work at the limit.
inputPricePerM float or null null Override input token price per 1M tokens in USD.
outputPricePerM float or null null Override output token price per 1M tokens in USD.
dataDir string "/var/lib/fiach" Directory to store state database and reports.
contextGroups attrset {} Context groups mapped by target repo (contains repos list).
sandbox.enable boolean false Enable Sandboxed PR reviews via systemd-nspawn.
sandbox.networkMode enum ("host", "bridge", "private", "veth") "veth" Network mode for the sandbox. veth isolates host-local services while retaining outbound NAT.
sandbox.extraArgs list of string [] Extra arguments to pass to systemd-nspawn.
sandbox.memoryMax systemd size "8G" Aggregate memory limit for the daemon and all sandbox workers.
sandbox.cpuQuota systemd percent "400%" Aggregate CPU quota for the daemon and all sandbox workers.
sandbox.tasksMax positive integer 4096 Aggregate task limit for the daemon and all sandbox workers.

Cost limits are enforced from provider-reported usage or configured token prices. Because usage is reported after a model response, a single in-flight response can overshoot the limit. If neither provider cost nor model pricing is available, Fiach logs that it cannot enforce the limit; set inputPricePerM and outputPricePerM to make enforcement deterministic.

NixOS Sandbox Network Examples

Default isolated veth networking:

services.fiach = {
  sandbox = {
    enable = true;
    networkMode = "veth";
  };
};

Bridge networking for hosts that already provide a br-nspawn bridge:

services.fiach = {
  sandbox = {
    enable = true;
    networkMode = "bridge";
  };
};

When using bridge, the host must create and maintain br-nspawn, addressing, DHCP or static addressing, forwarding, and NAT. Fiach passes --network-bridge=br-nspawn to systemd-nspawn; it does not create the bridge from the NixOS module.

Sandbox Networking Limits

systemd-nspawn can isolate the sandbox from the host network namespace, but it does not provide destination allowlisting such as "only GitHub and OpenRouter".

  • sandbox.networkMode = "host" is an explicit compatibility escape hatch that shares host networking.
  • sandbox.networkMode = "bridge" attaches each sandbox to an existing br-nspawn bridge.
  • sandbox.networkMode = "veth" is the default. It gives each sandbox its own 10.64.<index>.0/30 veth subnet, blocks access to host-local services, and configures NixOS NAT for outbound internet access from the 10.64.0.0/16 sandbox pool.
  • sandbox.networkMode = "private" disables all network access, which also prevents GitHub and OpenRouter access.
  • Restricting outbound traffic to specific destinations requires host-side enforcement such as nftables/iptables rules on the ve-* interfaces, or a proxy-based egress policy.
  • IP allowlists can be managed in NixOS firewall configuration, but they are brittle for CDN-backed services.
  • If you need domain-level guarantees, use a dedicated proxy or egress gateway; systemd-nspawn alone is not sufficient.

veth supports concurrent workers by allocating a unique /30 per active sandbox. Because the allocator uses the 10.64.1.0/30 through 10.64.254.0/30 pool, NixOS deployments using veth must set maxWorkers between 1 and 254; maxWorkers = 0 is rejected for this mode.

Sandbox Write Scope

In daemon sandbox mode, the sandbox no longer gets write access to the whole fiach data directory.

  • The sandbox writes only per-review artifacts in a dedicated run directory.
  • The sandbox stdout/stderr stream is saved as reports/runs/<repo>_PR<number>/nspawn.log by default.
  • Review state in fiach.redb is recorded by the host daemon after the sandbox exits successfully.
  • Disclosure side effects are also performed by the host daemon after it validates the sandbox output.
  • GitHub disclosure is journaled before publication and tagged with a hidden stable marker. After an interrupted publication, the host reconciles GitHub history before retrying and reuses an existing review or comment instead of duplicating it.

This reduces the impact of a malicious or prompt-injected agent: it can still produce a bad report, but it cannot directly corrupt the review database from inside the sandbox.

Domain Skills In Sandbox Mode

fiach looks for domain skills in this order:

  • ./.agents/skills in the active review workspace
  • /etc/fiach/skills packaged into the sandbox rootfs

This lets NixOS/systemd deployments keep using bundled skills such as rust-security even when the target PR repository does not contain its own .agents/skills directory.

🏗 Project Layout

  • src/main.rs: Clap input parsing and runtime service wiring.
  • src/scheduler.rs: Bounded FIFO scheduling and process-local job lifecycle.
  • src/execution.rs: Common local/sandbox executor outcome contract.
  • src/finalizer.rs: Host-only rendering, disclosure, reactions, and persistence.
  • src/github.rs: Object-safe GitHub boundary and the production gh adapter.
  • src/daemon.rs: Polling and sandbox/nspawn orchestration.
  • src/review.rs: Finder, verifier, and duplicate-adjudication agent passes.
  • src/server.rs: Axum-based interactive web server for daemon management and reporting.
  • src/workspace.rs: Manages cloning the repo and checking out the PR into a temporary directory.
  • src/disclose.rs: Handles the ReportMode logic (commenting or creating Sync PRs).
  • src/state.rs: Typed, versioned, async redb state adapter.