This is the full developer documentation for unfault
# Welcome to Unfault
> A CLI that finds production reliability issues in your code.
Unfault is an open-source CLI that analyzes your code for patterns that cause production problems.
It’s not a linter. It doesn’t police style or enforce conventions. It looks at how your code behaves at runtime: what calls can hang, where errors disappear, what fails under load, what breaks when a dependency goes down.
Run it before you push. Run it in CI. Pipe it into your AI agent. It tells you what’s actually there.
## Commands
[Section titled “Commands”](#commands)
* `unfault review`: narrative summary with the most important findings, each named and explained
* `unfault lint`: all findings grouped by severity and rule, linter-style
* `unfault graph`: impact analysis, dependency queries, hub file detection
* `unfault config`: configure LLM providers and inspect observability integrations
## Quick Links
[Section titled “Quick Links”](#quick-links)
[Installation ](/docs/installation/)Get unfault running in under a minute.
[Quick Start ](/docs/quick-start/)Run your first review and understand the output.
[Rules Catalog ](/docs/reference/rules/)Browse all the patterns unfault detects.
[How It Works ](/docs/concepts/how-it-works/)What gets parsed, what gets analyzed, what stays on your machine.
## Use It Where You Work
[Section titled “Use It Where You Work”](#use-it-where-you-work)
* **Terminal**: `unfault review` before you push. `unfault lint` to see everything.
* **CI/CD**: Gate on findings with exit codes. SARIF output for GitHub Code Scanning.
* **AI Agents**: `--output json` and `graph` commands are built for agent consumption. See [Use with AI Agents](/docs/guides/agents/).
## Philosophy
[Section titled “Philosophy”](#philosophy)
Production failures are usually visible in the code before they happen. The HTTP call with no timeout. The retry loop with no backoff. The error handler that swallows the exception. These aren’t hard to spot. They just don’t get spotted because nobody’s looking for them systematically.
Unfault looks for them. It won’t catch everything, and it doesn’t try to. It focuses on the patterns that have actually caused production incidents, explains the tradeoff in each case, and lets you decide what to do about it.
# Cognitive Context
> What cognitive context means and why it matters for writing better code.
There’s a gap between the code you write and the system it becomes.
You write a function. You know what it does in isolation. But do you know what happens when it runs alongside everything else? What services it depends on? What fails when it fails? What safeguards exist upstream?
This is what we mean by cognitive context: the awareness of how your code behaves as part of a running system, available while you’re still writing it.
## The Problem
[Section titled “The Problem”](#the-problem)
When you’re deep in a function, your mental model is local. You’re thinking about the logic at hand, the variables in scope, the immediate task. That’s normal. That’s how we work.
But production systems don’t run in isolation. A function that looks fine on its own might:
* Call an external service without a timeout, creating cascading failures
* Swallow an exception that should propagate, hiding real problems
* Depend on a database connection that has no retry logic
* Live in a route with no rate limiting, exposed to abuse
These aren’t bugs in the traditional sense. The code does what it says. The issue is that the code doesn’t say enough about how it behaves under stress, at scale, over time.
Most of the time, you don’t discover this until production tells you. And by then, you’re debugging at 2am with incomplete information and mounting pressure.
## What Traditional Tools Miss
[Section titled “What Traditional Tools Miss”](#what-traditional-tools-miss)
Linters check syntax and style. Type checkers verify contracts. Test suites confirm behavior under controlled conditions. These are valuable, but they don’t tell you:
* Where this function fits in the call graph
* What external dependencies it transitively relies on
* Whether error paths are handled or silently dropped
* How the framework wires this handler to incoming requests
Static analysis tools can find some of this, but they typically operate in batch mode. You run them in CI, get a report, and context-switch back to fix things. The insight arrives too late, disconnected from the moment of writing.
## Cognitive Context in Practice
[Section titled “Cognitive Context in Practice”](#cognitive-context-in-practice)
Unfault takes a different approach. Instead of generating reports, it provides context where you work:
**In your editor**, when you hover over a function, you might see:
* “Called by 3 routes, 2 with authentication middleware”
* “Makes HTTP calls to payment-service, no circuit breaker”
* “Error path logs but doesn’t propagate”
**In your terminal**, when you run a review:
* Findings grouped by what matters (stability, performance, security)
* Each finding explains why it matters, not just what it is
* Suggested fixes when the path forward is clear
**In CI**, as a gate:
* Catch patterns that tend to cause production issues
* Exit codes that integrate with your deployment flow
* Output formats for your existing tools
The goal isn’t to generate more alerts. It’s to give you the information you’d want if you could hold the entire system in your head.
## Why This Matters
[Section titled “Why This Matters”](#why-this-matters)
Code review catches a lot. Testing catches a lot. But some things slip through because they require thinking about the system as a whole, and that’s hard to do when you’re reviewing a diff or writing a unit test.
Consider a simple change: you add a call to fetch user preferences from a new service. The code is correct. The tests pass. The reviewer approves.
But nobody noticed:
* The new service has p99 latency of 800ms
* The call has no timeout
* It’s in the critical path for page load
* There’s no fallback if the service is down
Six months later, that service has a bad deploy, and your unrelated feature is suddenly the cause of a site-wide slowdown.
This isn’t a failure of process. It’s a failure of context. The information existed, but it wasn’t available at the moment of decision.
## Not a Gatekeeper
[Section titled “Not a Gatekeeper”](#not-a-gatekeeper)
Unfault doesn’t block your commits or fail your builds by default. It doesn’t tell you what to do. It surfaces information and lets you decide.
Sometimes a missing timeout is fine. Maybe the call is to a local cache with sub-millisecond latency. Maybe you’re in a batch job where latency doesn’t matter. You know your system better than any tool.
What Unfault does is make sure you’re making that decision consciously, with the relevant context visible, rather than accidentally.
## The Feeling We’re After
[Section titled “The Feeling We’re After”](#the-feeling-were-after)
You know that feeling when you’re working with someone who really knows the codebase? They glance at your PR and say, “Oh, be careful here, this function gets called from the payment flow and that service is flaky on Mondays.”
That’s the experience we’re building toward. Not a judge. Not an enforcer. Just a knowledgeable colleague who helps you see what you might have missed.
## Next Steps
[Section titled “Next Steps”](#next-steps)
How It Works
The architecture that makes this possible. [Read more](/docs/concepts/how-it-works/)
Workspaces
How Unfault identifies and tracks your project. [Read more](/docs/concepts/workspaces-sessions/)
Dimensions
How findings are categorized. [Read more](/docs/concepts/dimensions/)
VS Code Extension
Get context in your editor. [Read more](/docs/guides/vscode/)
# Dimensions
> How Unfault organizes facts by what matters to your system.
Unfault groups facts into dimensions based on what aspect of your system they affect. This isn’t just categorization for its own sake. It helps you focus on what matters for your particular situation.
A high-traffic API gateway cares deeply about performance. A financial service prioritizes correctness. A public-facing app needs to think about security. Dimensions let you filter facts to match your priorities.
## The Dimensions
[Section titled “The Dimensions”](#the-dimensions)
### Stability
[Section titled “Stability”](#stability)
How well does your system handle the unexpected?
Stability findings surface patterns that tend to cause cascading failures, hung requests, or unrecoverable states. Things like:
* External calls without timeouts
* Missing circuit breakers on flaky dependencies
* Unbounded retries that amplify load during outages
* Resource leaks that accumulate over time
These patterns often work fine in normal conditions. They become problems when something goes wrong elsewhere, and your code’s response makes it worse.
### Performance
[Section titled “Performance”](#performance)
Where might your system slow down under load?
Performance findings identify code that may become a bottleneck:
* Blocking I/O in async contexts
* N+1 query patterns
* CPU-intensive work on the event loop
* Regex compilation in hot paths
* Unbounded caches that grow forever
Not every performance finding needs immediate action. A slow path that runs once at startup is different from one in your request handler. The findings give you visibility; you decide what matters.
### Correctness
[Section titled “Correctness”](#correctness)
Does your code do what it should?
Correctness findings catch logic issues that could produce wrong results:
* Race conditions in shared state
* Recursive functions without base cases
* Unsafe deserialization of untrusted input
* Integer overflow in arithmetic operations
These are closer to traditional bugs, but Unfault looks for patterns that static analysis and tests often miss, especially concurrency issues and edge cases in error handling.
### Security
[Section titled “Security”](#security)
What attack surface does your code expose?
Security findings highlight potential vulnerabilities:
* SQL injection via string concatenation
* Command injection through unsanitized input
* Hardcoded credentials or secrets
* Missing authentication on sensitive routes
* Insecure defaults in cryptographic operations
Security findings tend to require careful evaluation. Context matters: an internal tool has different threat models than a public API.
### Reliability
[Section titled “Reliability”](#reliability)
Can your system recover gracefully?
Reliability overlaps with stability but focuses on recovery:
* Missing error handlers on critical paths
* Silent exception swallowing
* No fallback when dependencies fail
* Incomplete cleanup in error paths
A reliable system doesn’t just avoid failures; it handles them well when they occur.
### Observability
[Section titled “Observability”](#observability)
Can you see what’s happening?
Observability findings identify blind spots in your monitoring:
* Missing correlation IDs across service boundaries
* Log statements that omit crucial context
* Untracked external calls
* Missing metrics on key operations
When something goes wrong at 3am, observability determines whether you debug for 10 minutes or 10 hours.
### Scalability
[Section titled “Scalability”](#scalability)
What breaks when traffic grows?
Scalability findings look at patterns that work at low scale but fail at high scale:
* Linear scans where indexes exist
* Unbounded result sets without pagination
* Connection pooling issues
* Memory allocation patterns that fragment under load
### Maintainability
[Section titled “Maintainability”](#maintainability)
How hard is this code to change safely?
Maintainability is opt-in and not included in default analysis. When enabled, it identifies:
* High cyclomatic complexity
* Deep nesting
* Long functions
* Circular dependencies
These aren’t bugs. They’re friction. Code that’s hard to understand is code that’s easy to break.
Note
Maintainability findings are disabled by default because they’re more subjective. Enable them explicitly if code complexity metrics are useful for your team.
## Using Dimensions
[Section titled “Using Dimensions”](#using-dimensions)
### Filtering by Dimension
[Section titled “Filtering by Dimension”](#filtering-by-dimension)
Focus your review on specific concerns:
```bash
# Only stability issues
unfault review --dimension stability
# Stability and performance
unfault review --dimension stability --dimension performance
```
### Default Dimensions
[Section titled “Default Dimensions”](#default-dimensions)
By default, Unfault analyzes for:
* Stability
* Performance
* Correctness
* Security
* Reliability
* Observability
* Scalability
Maintainability is excluded unless explicitly requested.
### Choosing What Matters
[Section titled “Choosing What Matters”](#choosing-what-matters)
Different projects have different priorities:
| Project Type | Focus Dimensions |
| ----------------- | ----------------------------------- |
| High-traffic API | Stability, Performance, Scalability |
| Financial system | Correctness, Security, Reliability |
| Internal tool | Correctness, Maintainability |
| Public-facing app | Security, Stability, Performance |
There’s no universal answer. The dimensions are a lens, not a prescription.
## Dimensions vs. Severity
[Section titled “Dimensions vs. Severity”](#dimensions-vs-severity)
Dimensions and severity are orthogonal:
* **Dimension**: What aspect of the system is affected
* **Severity**: How urgent is this finding
A high-severity stability finding means “this will likely cause problems soon.” A low-severity performance finding means “this could matter at scale, but isn’t urgent.”
You might filter by dimension to focus your review, then prioritize by severity within that set.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Rules Catalog
See all rules organized by dimension. [Browse rules](/docs/reference/rules/)
Configuration
Customize which dimensions to analyze. [Read more](/docs/reference/configuration/)
CLI Usage
Filter findings in practice. [Read more](/docs/guides/cli/)
# Facts
> The building blocks of what Unfault knows about your code.
When Unfault analyzes your code, it produces **facts**. A fact is a discrete observation about your codebase: something Unfault noticed, measured, or inferred.
Facts are the foundation of everything Unfault tells you. They’re not opinions or suggestions. They’re observations that you can query, filter, aggregate, and reason about.
## What Facts Are
[Section titled “What Facts Are”](#what-facts-are)
A fact is a typed signal with context. Each fact has:
* **Type**: What kind of observation this is
* **Location**: Where in your code it applies (file, line, function)
* **Dimension**: What aspect of your system it relates to (stability, performance, security)
* **Severity**: How important this observation is
* **Payload**: The specific details of what was observed
Facts are objective. They describe what *is*, not what *should be*. The interpretation, whether something needs action, is up to you.
## Types of Facts
[Section titled “Types of Facts”](#types-of-facts)
### Findings
[Section titled “Findings”](#findings)
The most common fact type today is a **finding**: a pattern detected by a rule.
```plaintext
fact_type: rule_finding
rule_id: python.http.missing_timeout
severity: High
dimension: stability
file: src/client.py
line: 42
payload:
title: "HTTP call has no timeout"
description: "This requests.get call will block indefinitely if the server doesn't respond."
fix_preview: "Add timeout parameter: requests.get(url, timeout=30)"
```
Findings tell you “here’s a pattern that often causes problems.” They’re actionable observations with suggested fixes.
### SLO State (Coming)
[Section titled “SLO State (Coming)”](#slo-state-coming)
Note
SLO facts are in development. This describes where we’re headed.
When you link SLOs to your codebase, Unfault can capture their current state as facts:
```plaintext
fact_type: slo_state
slo_name: "API Availability"
provider: gcp
target_percent: 99.9
current_percent: 99.87
error_budget_remaining: -0.03
dimension: observability
routes_covered: 15
```
SLO facts tell you “here’s what your observability systems are reporting.” They connect your code to production reality.
### More Fact Types
[Section titled “More Fact Types”](#more-fact-types)
The fact model is extensible. Future fact types might include:
* **Dependency state**: Vulnerability status, version drift, license compliance
* **Test coverage**: Which code paths lack test coverage
* **Call patterns**: HTTP calls, database queries, external service dependencies
* **Complexity metrics**: Cyclomatic complexity, coupling, cohesion
Each fact type adds a new lens for understanding your code.
## Facts vs. Findings
[Section titled “Facts vs. Findings”](#facts-vs-findings)
If you’ve used Unfault before, you’re familiar with “findings.” Here’s how they relate:
| | Findings | Facts |
| ------------ | -------------------------- | -------------------------------------- |
| **Scope** | Rule-detected patterns | Any observation about your code |
| **Action** | Usually suggests a fix | May or may not be actionable |
| **Tone** | ”Here’s a problem" | "Here’s what I observed” |
| **Examples** | Missing timeout, N+1 query | Finding, SLO state, dependency version |
Findings are facts. They’re the `rule_finding` fact type. But facts are broader: they’re the general model for everything Unfault knows.
Tip
Think of facts as storytelling about your codebase. Findings are one chapter, focused on patterns that might cause problems. Other chapters tell different stories: how your SLOs are doing, what your dependencies look like, how your code has changed.
## Working with Facts
[Section titled “Working with Facts”](#working-with-facts)
### Querying Facts
[Section titled “Querying Facts”](#querying-facts)
Today, facts are surfaced through `unfault review` and `unfault graph` commands. Future releases will add more direct querying capabilities:
```bash
# Surface findings facts
unfault review --output json
# Surface graph facts
unfault graph critical --json
unfault graph impact src/api/client.py --json
```
### Facts in Sessions
[Section titled “Facts in Sessions”](#facts-in-sessions)
Every analysis session produces facts. When you run `unfault review`, the facts from that run are stored and associated with your workspace.
```plaintext
Session (Jan 10, 2025)
├── 12 rule_finding facts
├── 2 slo_state facts (coming)
└── Graph data
```
This history lets you track how facts change over time. Are findings increasing or decreasing? Is your SLO compliance improving?
### Facts and Insights
[Section titled “Facts and Insights”](#facts-and-insights)
Unfault aggregates facts into **insights**: high-level summaries that help you understand patterns across your codebase.
For example, facts might show 47 individual findings across 185 files. The summary output might observe “the `src/clients/` directory has the highest concentration of stability issues, primarily missing timeouts and circuit breakers.”
Insights are derived from facts. As more fact types are added, insights become richer.
## Why Facts Matter
[Section titled “Why Facts Matter”](#why-facts-matter)
The shift to facts reflects how we think about code understanding:
**Facts are composable.** You can combine findings with SLO data to ask “which stability findings affect routes that are close to burning error budget?” That question spans two fact types.
**Facts are queryable.** Instead of just getting a report, you can ask questions. The fact model makes your codebase knowledge queryable.
**Facts tell a story.** A finding says “here’s a problem.” A collection of facts says “here’s what’s happening in your system.” The story is richer than any single observation.
**Facts scale.** As Unfault learns more about your code, it produces more fact types. The model grows without changing fundamentally.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Dimensions
How facts are categorized by what they affect. [Read more](/docs/concepts/dimensions/)
How It Works
The full analysis pipeline. [Read more](/docs/concepts/how-it-works/)
Query the Codebase
Graph and review commands for exploration. [Read more](/docs/guides/asking-questions/)
# How It Works
> Understand what happens when you run a review, entirely on your machine.
When you run `unfault review`, everything happens locally. There is no API, no cloud analysis, no data leaving your machine. Understanding this helps explain why Unfault behaves the way it does.
## The Short Version
[Section titled “The Short Version”](#the-short-version)
1. **Parse locally.** Unfault reads your source files using Tree-sitter and extracts a semantic model: functions, calls, imports, routes.
2. **Build a code graph.** File-level semantics are merged into a unified graph capturing how things connect across your whole project.
3. **Run analysis in-process.** The `unfault-analysis` engine runs rules against the graph and produces findings.
4. **Show results.** Findings appear in your terminal (or CI output), formatted for the mode you chose.
## Why Local Analysis?
[Section titled “Why Local Analysis?”](#why-local-analysis)
Most analysis tools either send your source code to a server or require heavy language servers. Unfault does neither.
Parsing and analysis run in the same process as the CLI, using the same code your project runs with. This means:
* **Privacy.** Your source code never leaves your machine.
* **Speed.** Parsing runs in parallel on your hardware. No round-trips.
* **No account needed.** The core CLI is open source and works offline.
* **Consistency.** The same analysis runs on your laptop and in CI.
## What Gets Extracted
[Section titled “What Gets Extracted”](#what-gets-extracted)
When Unfault parses your code, it builds a graph with nodes and edges:
**Nodes** represent things in your code:
* Files
* Functions and methods
* Classes
* Imports (internal and external)
* Framework constructs (routes, middleware, handlers)
**Edges** represent relationships:
* Contains (file contains function)
* Calls (function A calls function B)
* Imports (file imports module)
* Inherits (class extends another)
* Framework wiring (app registers route, route uses middleware)
This graph captures the *structure* of your code without the *content*. The analysis engine can reason about “function `fetch_user` calls `requests.get` with no timeout” without touching the actual URL or request body.
## The Analysis Flow
[Section titled “The Analysis Flow”](#the-analysis-flow)
Here’s what happens when you run a review:
### 1. Workspace Detection
[Section titled “1. Workspace Detection”](#1-workspace-detection)
Unfault scans your project to understand what it’s looking at:
* Which languages are present (Python, Go, Rust, TypeScript/JavaScript)
* Which frameworks are in use (FastAPI, Express, Gin, Axum, Next.js, etc.)
* Project structure and entry points
### 2. Parsing and Semantic Extraction
[Section titled “2. Parsing and Semantic Extraction”](#2-parsing-and-semantic-extraction)
For each source file, Unfault:
* Parses the syntax tree with Tree-sitter
* Extracts semantic information (functions, classes, calls, imports)
* Detects framework-specific patterns (route decorators, middleware registration)
* Builds the local portion of the code graph
### 3. Graph Construction
[Section titled “3. Graph Construction”](#3-graph-construction)
Individual file semantics get merged into a unified graph. This is where Unfault resolves:
* Which function calls go where
* How imports connect files
* Framework topology (which routes exist, what middleware applies)
### 4. Rule Analysis
[Section titled “4. Rule Analysis”](#4-rule-analysis)
The `unfault-analysis` engine runs rules against the graph. Rules are organized by framework profile (e.g., `python_fastapi_backend`, `go_gin_service`) and dimension (stability, correctness, performance, scalability).
Each rule produces **findings**: observations about your code. A finding includes what was detected, where it is, why it matters, and a suggested fix when possible.
### 5. Enrichment (Optional)
[Section titled “5. Enrichment (Optional)”](#5-enrichment-optional)
If you have observability integrations configured (GCP Cloud Monitoring, Datadog, Dynatrace), Unfault can fetch SLO data and enrich findings with production context: which routes have SLO coverage and which don’t. This step is skipped with `--offline`.
### 6. Output
[Section titled “6. Output”](#6-output)
Results are formatted for your chosen output mode and printed to stdout.
## What the Analysis Sees
[Section titled “What the Analysis Sees”](#what-the-analysis-sees)
To be concrete, if you have this code:
```python
def fetch_user(user_id: str) -> dict:
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
```
The analysis sees something like:
```json
{
"functions": [{
"name": "fetch_user",
"file": "users.py",
"calls": [{"target": "requests.get", "has_timeout": false}]
}]
}
```
No URL. No variable names beyond what’s needed for the graph. No string literals. Just enough structure to identify the pattern.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Dimensions
How findings are categorized. [Read more](/docs/concepts/dimensions/)
Explore the Code Graph
Impact analysis, dependency queries, critical files. [Read more](/docs/guides/code-graph/)
Quick Start
Run your first review. [Get started](/docs/quick-start/)
CLI Reference
All commands and flags. [Read more](/docs/reference/cli/)
# Workspaces
> How Unfault identifies and tracks your project across runs.
When you run `unfault review`, Unfault identifies which project it’s analyzing through a **workspace**. Understanding this helps explain how Unfault detects your project, applies configuration, and displays results.
## What a Workspace Is
[Section titled “What a Workspace Is”](#what-a-workspace-is)
A **workspace** is Unfault’s representation of a project or codebase. It determines:
* Which configuration files apply
* What the workspace is called in output
* How Unfault identifies the project for SLO linking
The workspace is derived from the directory you run Unfault from. Everything happens locally.
## How Workspaces Are Identified
[Section titled “How Workspaces Are Identified”](#how-workspaces-are-identified)
Unfault computes a workspace label from your project. The source depends on what’s available:
1. **Git remote** (most stable): If your project has a git remote configured, Unfault uses that URL to compute a consistent label. The same repo analyzed from different directories gets the same workspace identity.
2. **Project manifest** (fallback): If there’s no git remote, Unfault looks at manifest files like `pyproject.toml`, `package.json`, or `go.mod` to identify the project.
3. **Directory name** (least stable): As a last resort, Unfault uses the current directory name. This is less reliable because renaming a directory changes the workspace identity.
Tip
For consistent identification across machines and CI, make sure your project has a git remote configured. This gives Unfault the most stable workspace identity.
## Workspace Label
[Section titled “Workspace Label”](#workspace-label)
The workspace label is the human-readable name you see in analysis output. By default, it’s your directory name (e.g., `payments-service`).
```plaintext
1112ms - python / fastapi - 1 file [payments-service]
```
The label is display-only. It doesn’t affect which files are analyzed or how rules apply.
## Configuration and Workspaces
[Section titled “Configuration and Workspaces”](#configuration-and-workspaces)
Each workspace can have its own configuration. Unfault looks for configuration in:
1. Current directory’s manifest file (`pyproject.toml`, `Cargo.toml`, `package.json`)
2. Parent directories up to the git root
3. `.unfault.toml` if no manifest contains Unfault configuration
This means running `unfault review` from different directories in the same repository applies different configuration. A monorepo with per-service config in each subdirectory works naturally.
See [Configuration](/docs/reference/configuration/) for the full reference.
## Workspace-Level SLO Linking
[Section titled “Workspace-Level SLO Linking”](#workspace-level-slo-linking)
When Unfault detects cloud credentials and links SLOs to your codebase, that mapping is stored locally at `.unfault/` in your project directory:
```plaintext
.unfault/
└── cache/
└── enrichment/
└── slo_mapping.json
```
This file persists across runs. Future reviews remember which SLOs belong to this workspace without prompting again. To re-run discovery and update the mapping, force a cache refresh:
```bash
unfault review --refresh-cache
```
## Enrichment Cache
[Section titled “Enrichment Cache”](#enrichment-cache)
To avoid fetching SLO and trace data on every run, Unfault caches observability data locally:
```plaintext
.unfault/cache/enrichment/
```
The cache has a 5-minute TTL. You’ll see `cached` or `fetch Xms` in the review footer accordingly. Force a cache refresh with:
```bash
unfault review --refresh-cache
```
Skip enrichment entirely (for CI or offline use) with:
```bash
unfault review --offline
```
## Privacy
[Section titled “Privacy”](#privacy)
All workspace data stays on your machine:
* **Stored locally**: Workspace labels, SLO mappings, enrichment cache
* **Never stored remotely**: Your source code, string literals, comments, or variable values
Analysis runs entirely on your hardware. See [How It Works](/docs/concepts/how-it-works/) for details.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Configuration
Customize behavior per workspace. [Read more](/docs/reference/configuration/)
SLO Discovery
Link cloud SLOs to your workspace. [Read more](/docs/guides/slo-discovery/)
How It Works
The full local analysis pipeline. [Read more](/docs/concepts/how-it-works/)
# How to Contribute
> Get started contributing to Unfault.
Unfault’s client-side tools are open source, and we welcome contributions. Whether you’re fixing a bug, improving parsing for a language, or enhancing the VS Code extension, there’s a place for your work.
## The Open Source Ecosystem
[Section titled “The Open Source Ecosystem”](#the-open-source-ecosystem)
The following repositories are open source and accept contributions:
| Repository | Language | What it does | License |
| --------------------------------------------------- | ---------- | ------------------------------------------------ | ------- |
| [unfault/cli](https://github.com/unfault/cli) | Rust | Command-line interface, orchestrates analysis | MIT |
| [unfault/core](https://github.com/unfault/core) | Rust | Parsing, semantic extraction, graph construction | MIT |
| [unfault/vscode](https://github.com/unfault/vscode) | TypeScript | VS Code extension with LSP | MIT |
Most contributions fall into one of these areas:
Fix bugs
Found something broken? We’d love a fix. [View issues](https://github.com/unfault/cli/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
Improve parsing
Help Unfault understand more languages and frameworks. [Read more](/docs/contributing/architecture/)
Enhance the extension
Improve the VS Code experience. [View issues](https://github.com/unfault/vscode/issues)
Improve docs
Clarify confusing sections or add missing information. [View on GitHub](https://github.com/unfault/unfault/tree/main/www)
## Quick Start
[Section titled “Quick Start”](#quick-start)
### Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* **Rust 1.70+** for CLI and core work
* **Node.js 18+** for VS Code extension and documentation
* **Git** for version control
### Clone and Build
[Section titled “Clone and Build”](#clone-and-build)
```bash
# CLI
git clone https://github.com/unfault/cli
cd cli && cargo build
# Core library
git clone https://github.com/unfault/core
cd core && cargo build
# VS Code extension
git clone https://github.com/unfault/vscode
cd vscode && npm install && npm run compile
```
## Contribution Workflow
[Section titled “Contribution Workflow”](#contribution-workflow)
1. **Find or create an issue** describing what you want to work on
2. **Fork the repository** and create a branch
3. **Make your changes** following the code style guidelines
4. **Write tests** for new functionality
5. **Submit a pull request** with a clear description
### Commit Messages
[Section titled “Commit Messages”](#commit-messages)
We use [Conventional Commits](https://www.conventionalcommits.org/):
```plaintext
feat(parser): add support for Go generics
fix(cli): handle expired tokens gracefully
docs(readme): clarify installation steps
```
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
### Code Review
[Section titled “Code Review”](#code-review)
All changes go through code review. We look for:
* **Correctness**: Does it work? Are edge cases handled?
* **Tests**: Is new functionality tested?
* **Style**: Does it follow project conventions?
* **Documentation**: Are public APIs documented?
Be patient. Maintainers review PRs as time allows. If your PR sits for a week without feedback, a gentle ping is fine.
## Where to Start
[Section titled “Where to Start”](#where-to-start)
### Good First Issues
[Section titled “Good First Issues”](#good-first-issues)
Look for issues labeled `good first issue`:
* [CLI good first issues](https://github.com/unfault/cli/labels/good%20first%20issue)
* [Core good first issues](https://github.com/unfault/core/labels/good%20first%20issue)
* [VS Code good first issues](https://github.com/unfault/vscode/labels/good%20first%20issue)
### Documentation
[Section titled “Documentation”](#documentation)
Documentation improvements are always welcome. The docs live in `www/src/content/docs/` and use MDX (Markdown with components).
```bash
cd unfault/www
npm install
npm run dev
# Open http://localhost:4321
```
### Parsing and Semantics
[Section titled “Parsing and Semantics”](#parsing-and-semantics)
The `core` crate handles all client-side parsing and semantic extraction. If you want Unfault to better understand a language or framework:
1. Check the existing parsers in `core/src/parse/`
2. Check the semantic extractors in `core/src/semantics/`
3. Open an issue describing what’s missing
4. Submit a PR with tests
See the [Architecture](/docs/contributing/architecture/) page for details on how parsing works.
## Code of Conduct
[Section titled “Code of Conduct”](#code-of-conduct)
Be kind. Be constructive. Be professional. We’re all here to build something useful.
Harassment, discrimination, and toxic behavior have no place in this project. If you experience or witness unacceptable behavior, report it to .
## Getting Help
[Section titled “Getting Help”](#getting-help)
* **GitHub Discussions**: [unfault/cli/discussions](https://github.com/unfault/cli/discussions)
* **Existing Issues**: Search before opening a new one
* **Documentation**: You’re reading it
## Recognition
[Section titled “Recognition”](#recognition)
Contributors are recognized in release notes and the project README. Significant contributions may earn a spot in the maintainers list.
We appreciate every contribution, from typo fixes to major features. Thank you for helping make Unfault better.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Architecture
Understand how the client-side pieces fit together. [Read more](/docs/contributing/architecture/)
CLI Repository
Browse the CLI source code. [View on GitHub](https://github.com/unfault/cli)
# Architecture
> How Unfault's open source components work together.
This document explains how Unfault’s client-side components work. If you’re contributing to the CLI, core library, or VS Code extension, this context helps you understand where your changes fit.
## System Overview
[Section titled “System Overview”](#system-overview)
Unfault runs entirely on your machine:
### Key Principle: Local Analysis
[Section titled “Key Principle: Local Analysis”](#key-principle-local-analysis)
**Everything runs on your machine.** The CLI and VS Code extension use the `core` library to parse source code and build a semantic graph. The `analysis` crate then runs rules against that graph in-process. No source code, IR, or findings leave your machine.
This architecture provides:
* **Privacy**: Your code never leaves your machine
* **Speed**: Parsing and analysis are parallel and local
* **Offline support**: Works without network access
* **Consistency**: Same analysis logic in CLI and extension
## Components
[Section titled “Components”](#components)
### Core Library (`core/`)
[Section titled “Core Library (core/)”](#core-library-core)
The core library is the foundation of client-side analysis. It handles:
1. **Parsing**: Tree-sitter grammars for Python, Go, Rust, TypeScript
2. **Semantic Extraction**: Functions, classes, imports, calls
3. **Framework Analysis**: FastAPI routes, Express middleware, Gin handlers
4. **Graph Construction**: Nodes (files, functions) and edges (calls, imports)
5. **IR Generation**: Serialization for the API
```plaintext
core/
├── src/
│ ├── lib.rs # Public API
│ ├── parse/ # Tree-sitter parsing
│ │ ├── python.rs
│ │ ├── go.rs
│ │ ├── rust.rs
│ │ └── typescript.rs
│ ├── semantics/ # Semantic extraction
│ │ ├── mod.rs
│ │ ├── python/ # Python-specific semantics
│ │ ├── go/
│ │ └── ...
│ ├── graph/ # Graph construction
│ │ ├── mod.rs # CodeGraph implementation
│ │ ├── nodes.rs # Node types
│ │ └── edges.rs # Edge types
│ └── ir.rs # Intermediate representation
```
#### CodeGraph
[Section titled “CodeGraph”](#codegraph)
The `CodeGraph` is the central data structure:
```rust
pub struct CodeGraph {
nodes: Vec,
edges: Vec,
// Indexes for efficient lookups
file_index: HashMap,
function_index: HashMap,
}
pub enum GraphNode {
File { path: PathBuf, language: Language },
Function { name: String, qualified_name: String, ... },
Class { name: String, ... },
ExternalModule { name: String, category: ModuleCategory },
// Framework-specific nodes
Route { method: HttpMethod, path: String, ... },
Middleware { name: String, ... },
}
pub enum GraphEdgeKind {
Contains, // File contains Function
Calls, // Function calls Function
Imports, // File imports Module
Inherits, // Class inherits Class
UsesLibrary, // Function uses external library
// Framework-specific edges
RegistersRoute,
AppliesMiddleware,
}
```
### CLI (`cli/`)
[Section titled “CLI (cli/)”](#cli-cli)
The CLI orchestrates the analysis workflow:
```plaintext
cli/
├── src/
│ ├── main.rs # Entry point, argument parsing
│ ├── commands/ # Subcommands
│ │ ├── review.rs # `unfault review`
│ │ ├── lint.rs # `unfault lint`
│ │ ├── graph.rs # `unfault graph`
│ │ ├── info.rs # `unfault info`
│ │ ├── config.rs # `unfault config`
│ │ ├── lsp.rs # `unfault lsp`
│ │ └── agent_skills.rs # `unfault config agent`
│ ├── session/ # Analysis session management
│ │ ├── mod.rs # Session lifecycle
│ │ └── workspace.rs # Workspace detection
│ └── integration/ # Observability integrations
│ └── (gcp, datadog, dynatrace)
```
The review flow:
1. **Workspace Detection**: Scan for languages, frameworks, config files
2. **File Collection**: Select files based on hints and exclusions
3. **Parsing**: Use `core` to parse files and build graph
4. **IR Generation**: Serialize graph to JSON (intermediate representation)
5. **Local Analysis**: Pass IR to `unfault-analysis` which runs rules in-process
6. **Output**: Format and display findings
### VS Code Extension
[Section titled “VS Code Extension”](#vs-code-extension)
The extension provides real-time analysis via LSP:
```plaintext
vscode/
├── src/
│ ├── extension.ts # Extension entry point
│ ├── contextView.ts # Context sidebar webview
│ └── welcomePanel.ts # Onboarding UI
```
The extension spawns the CLI in LSP mode (`unfault lsp`) and communicates via the Language Server Protocol. The CLI handles all parsing and analysis; the extension focuses on UI.
## Data Flow
[Section titled “Data Flow”](#data-flow)
### Review Flow
[Section titled “Review Flow”](#review-flow)
### Graph Query Flow
[Section titled “Graph Query Flow”](#graph-query-flow)
## Key Design Decisions
[Section titled “Key Design Decisions”](#key-design-decisions)
### Why Local Analysis?
[Section titled “Why Local Analysis?”](#why-local-analysis)
Analysis runs entirely on your machine:
1. **Privacy**: Source code never leaves your system
2. **Speed**: No round-trips, parsing runs in parallel on your hardware
3. **Offline support**: Works without network access
4. **Consistency**: Same analysis in CLI and editor extension
### Why a Separate Core Library?
[Section titled “Why a Separate Core Library?”](#why-a-separate-core-library)
The `core` library is a separate crate from the CLI. This is good practice:
* Clear separation of concerns (parsing vs. orchestration vs. analysis)
* Easier testing (test parsing logic independently)
* Reusable foundation for future clients
### Why a Separate Analysis Library?
[Section titled “Why a Separate Analysis Library?”](#why-a-separate-analysis-library)
The `analysis` crate lives separately from `core` and `cli`:
* Rules are testable in isolation
* New rules can be added without touching parsing logic
* Profiles (rule sets) are decoupled from both parsing and output
### What Does the IR Contain?
[Section titled “What Does the IR Contain?”](#what-does-the-ir-contain)
The Intermediate Representation (IR) passed from `core` to `analysis` contains:
* File paths and languages
* Function names and signatures
* Import relationships
* Call relationships
* Framework topology (routes, middleware)
The IR does **not** contain:
* Source code
* String literals
* Comments
* Variable values
## Contributing to Client-Side Code
[Section titled “Contributing to Client-Side Code”](#contributing-to-client-side-code)
### Adding Language Support
[Section titled “Adding Language Support”](#adding-language-support)
To add support for a new language:
1. **Add Tree-sitter grammar** to `core/Cargo.toml`
2. **Create parser** in `core/src/parse/{language}.rs`
3. **Create semantic extractor** in `core/src/semantics/{language}/`
4. **Add to `SourceSemantics` enum**
5. **Update CLI** language detection
6. **Add tests** with sample code
Caution
Adding a language enables parsing and graph construction. Rules for the new language also need to be added to the `analysis` crate. Open an issue to discuss rule coverage before writing a parser.
### Adding Framework Detection
[Section titled “Adding Framework Detection”](#adding-framework-detection)
To add support for a new framework:
1. **Add detection logic** in `core/src/semantics/{language}/frameworks/`
2. **Add framework-specific nodes/edges** if needed
3. **Update `FrameworkGuess`** signals
4. **Add tests** with sample framework code
### Improving Semantic Extraction
[Section titled “Improving Semantic Extraction”](#improving-semantic-extraction)
To capture more semantic information:
1. **Extend the model** in `core/src/semantics/{language}/model.rs`
2. **Update extraction logic** in `core/src/semantics/{language}/mod.rs`
3. **Add tests** covering the new extraction
### Adding an Observability Provider
[Section titled “Adding an Observability Provider”](#adding-an-observability-provider)
The CLI can discover SLOs from observability platforms and link them to route handlers. Currently supported: GCP Cloud Monitoring, Datadog, Dynatrace.
To add a new provider:
1. **Create provider module** in `cli/src/slo/{provider}.rs`
2. **Implement credential detection** (environment variables, config files)
3. **Implement `fetch_slos`** to query the provider’s API
4. **Return `SloDefinition`** structs with name, target, path pattern
5. **Register in `SloEnricher`** (`cli/src/slo/mod.rs`)
6. **Add environment variables** to documentation
Example provider structure:
```rust
pub struct MyProvider {
api_key: String,
endpoint: String,
}
impl MyProvider {
pub fn is_available() -> bool {
env::var("MY_PROVIDER_API_KEY").is_ok()
}
pub fn from_env() -> Option {
let api_key = env::var("MY_PROVIDER_API_KEY").ok()?;
Some(Self { api_key, endpoint: "https://api.myprovider.com".into() })
}
pub async fn fetch_slos(&self, client: &Client) -> Result> {
// Query API and map to SloDefinition
}
}
```
## Testing
[Section titled “Testing”](#testing)
### Core Tests
[Section titled “Core Tests”](#core-tests)
```bash
cd core
cargo test # All tests
cargo test python # Python-related tests
cargo test semantics::python # Specific module
```
### CLI Tests
[Section titled “CLI Tests”](#cli-tests)
```bash
cd cli
cargo test
```
### End-to-End Testing
[Section titled “End-to-End Testing”](#end-to-end-testing)
```bash
# Run the full workspace tests
cargo test --workspace
```
## Next Steps
[Section titled “Next Steps”](#next-steps)
How to Contribute
Contribution workflow. [Read more](/docs/contributing/)
CLI Repository
Browse the CLI source code. [View on GitHub](https://github.com/unfault/cli)
Core Repository
Parsing and semantics. [View on GitHub](https://github.com/unfault/core)
# Use with AI Agents
> Help AI coding assistants get codebase context and review changes.
AI coding assistants can write code fast. The failure mode is rarely syntax. It’s misunderstanding: wrong entry point, wrong pattern, missing context about what depends on what.
Unfault helps with that. Its graph commands surface codebase structure quickly, and its JSON output is built to be parsed and acted on by agents.
## Set Up Agent Skills
[Section titled “Set Up Agent Skills”](#set-up-agent-skills)
The quickest way to give your agent access to Unfault is to generate skill files:
```bash
# For Claude Code
unfault config agent claude
# For OpenCode
unfault config agent opencode
# Install globally (shared across projects)
unfault config agent claude --global
```
This writes structured skill definitions to `.claude/skills/` or `.opencode/skills/` so the agent knows what commands are available and how to use them.
## Manual Setup
[Section titled “Manual Setup”](#manual-setup)
If your agent doesn’t support skills, add this to your agent’s instruction file:
* GitHub Copilot: `.github/copilot-instructions.md`
* Cursor: `.cursorrules` or `.cursor/rules/*.mdc`
* Claude Code: `CLAUDE.md`
* OpenCode: `AGENTS.md`
```text
# Unfault (codebase context)
Use Unfault to understand the codebase before and after edits.
Find symbols (semantic search):
unfault graph find is not yet available. Use grep or search tools instead.
Estimate blast radius before refactoring:
unfault graph impact
unfault graph function-impact :
Find all files using a library:
unfault graph library
Find the most connected files:
unfault graph critical
Review the current codebase:
unfault review --output json
unfault review --dimension stability
```
## The Context Loop
[Section titled “The Context Loop”](#the-context-loop)
A useful pattern before making significant changes:
```bash
# 1. Understand what depends on what you're touching
unfault graph impact src/utils/auth.py
# 2. Check the most critical files for context
unfault graph critical --limit 5
# 3. Review the current state of the dimension you're working in
unfault review --dimension stability --output json
```
## Interpreting Review Output
[Section titled “Interpreting Review Output”](#interpreting-review-output)
When `unfault review --output json` returns:
* Look at `contexts[].findings`. Each finding has `file_path`, `line`, `severity`, `title`, and `fix_preview`.
* Start with `High` and `Critical` severity findings.
* `fix_preview` is a hint, not a complete patch. Use it as a starting point.
* If a finding is in code you didn’t touch, note it but don’t fix it in the same PR unless it’s directly relevant.
## Related Guides
[Section titled “Related Guides”](#related-guides)
* [Explore the Code Graph](/docs/guides/code-graph/)
* [CLI Reference](/docs/reference/cli/)
# Querying the Codebase
> Use graph commands to explore and understand your codebase structure.
The `unfault graph` commands let you ask structural questions about your codebase. Before making changes, running a migration, or onboarding to unfamiliar code, these queries help you understand what’s there and what might break.
## What You Can Ask
[Section titled “What You Can Ask”](#what-you-can-ask)
All graph commands run locally against the code graph built during analysis. No network required.
### What depends on a file?
[Section titled “What depends on a file?”](#what-depends-on-a-file)
```bash
unfault graph impact src/api/auth.py
```
Shows direct and transitive dependents. Use this before refactoring to understand blast radius.
### What depends on a function?
[Section titled “What depends on a function?”](#what-depends-on-a-function)
```bash
unfault graph function-impact src/api/auth.py:validate_token
```
Narrower than file-level impact. Useful when you’re changing a single function, not an entire module.
### What does this file depend on?
[Section titled “What does this file depend on?”](#what-does-this-file-depend-on)
```bash
unfault graph deps src/api/routes/payments.py
```
Shows internal imports and external libraries. Helps you understand the full picture before touching a file.
### Which files use a library?
[Section titled “Which files use a library?”](#which-files-use-a-library)
```bash
unfault graph library requests
```
Useful when migrating dependencies, auditing security vulnerabilities, or understanding how broadly a library is used.
### Which files are the most critical?
[Section titled “Which files are the most critical?”](#which-files-are-the-most-critical)
```bash
unfault graph critical
```
Returns files sorted by how many other files depend on them. These are your load-bearing files, the ones where changes have the widest effect.
### What does the codebase look like at a glance?
[Section titled “What does the codebase look like at a glance?”](#what-does-the-codebase-look-like-at-a-glance)
```bash
unfault graph stats
```
A quick overview: file count, function count, total edges, external library usage.
## Combining Queries
[Section titled “Combining Queries”](#combining-queries)
A practical workflow before a significant change:
```bash
# 1. Understand how central the file is
unfault graph impact src/core/models.py
# 2. See what it depends on
unfault graph deps src/core/models.py
# 3. Find files using any libraries involved
unfault graph library sqlalchemy
```
## JSON Output
[Section titled “JSON Output”](#json-output)
All graph commands support `--json` for programmatic use or scripting:
```bash
unfault graph impact --json src/api/auth.py
unfault graph critical --json --limit 5
unfault graph library --json requests
```
This is useful for CI checks, custom tooling, or feeding context to AI assistants.
## Review as a Query
[Section titled “Review as a Query”](#review-as-a-query)
The review command itself is a form of query: “what patterns in this codebase tend to cause production problems?”
```bash
# Everything
unfault review
# Only stability patterns
unfault review --dimension stability
# Only performance patterns
unfault review --dimension performance
# Detailed output with suggested fixes
unfault review --output full
# Machine-readable output
unfault review --output json
```
Tip
For understanding code patterns and conventions in an unfamiliar codebase, the graph commands answer structural questions; the review command answers operational ones. Both together give a thorough picture before you start changing things.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Explore the Code Graph
Detailed guide to all graph commands with examples. [Read more](/docs/guides/code-graph/)
CLI Reference
Full option reference for every command. [Read more](/docs/reference/cli/)
Use with AI Agents
Integrate graph output with coding assistants. [Read more](/docs/guides/agents/)
# CI/CD Integration
> Adding Unfault to your continuous integration pipeline.
Unfault integrates with GitHub Actions, GitLab CI, and other CI/CD systems. All analysis runs locally. No credentials or external services are required for the core review.
## Output Formats
[Section titled “Output Formats”](#output-formats)
| Format | Flag | Use Case |
| ------- | ---------------- | --------------------------------------------- |
| `basic` | `--output basic` | Human-readable terminal output (default) |
| `json` | `--output json` | Machine-readable JSON for custom integrations |
| `sarif` | `--output sarif` | SARIF format for GitHub Code Scanning |
Tip
Use `--output sarif` for seamless integration with GitHub’s security features, including inline annotations in pull requests and the Security tab.
## GitHub Actions
[Section titled “GitHub Actions”](#github-actions)
Add this workflow to `.github/workflows/unfault.yml`:
```yaml
name: Unfault
on:
pull_request:
push:
branches: [main]
jobs:
unfault:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Unfault
run: |
mkdir -p ~/.local/bin
curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-x86_64-unknown-linux-gnu
chmod +x ~/.local/bin/unfault
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Run Unfault review
run: unfault review --output sarif --offline > results.sarif
- name: Upload SARIF to GitHub
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
```
Note `--offline`: this skips SLO/trace fetching, which requires observability credentials not typically present in CI. Remove it if you have those credentials configured.
## CI Platforms
[Section titled “CI Platforms”](#ci-platforms)
* GitLab CI
Add to `.gitlab-ci.yml`:
```yaml
unfault:
image: debian:bookworm-slim
stage: test
before_script:
- apt-get update && apt-get install -y curl
- mkdir -p ~/.local/bin
- curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-x86_64-unknown-linux-gnu
- chmod +x ~/.local/bin/unfault
- export PATH="$HOME/.local/bin:$PATH"
script:
- unfault review --output sarif --offline > gl-code-quality-report.json
artifacts:
reports:
sast: gl-code-quality-report.json
```
Note
GitLab supports SARIF format for its [SAST reports](https://docs.gitlab.com/ee/user/application_security/sast/). Results appear in merge request security widgets.
* CircleCI
Add to `.circleci/config.yml`:
```yaml
version: 2.1
jobs:
unfault:
docker:
- image: cimg/base:current
steps:
- checkout
- run:
name: Install Unfault
command: |
mkdir -p ~/.local/bin
curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-x86_64-unknown-linux-gnu
chmod +x ~/.local/bin/unfault
echo 'export PATH="$HOME/.local/bin:$PATH"' >> $BASH_ENV
- run:
name: Run Unfault
command: unfault review --output sarif --offline > results.sarif
- store_artifacts:
path: results.sarif
workflows:
check:
jobs:
- unfault
```
## Exit Codes
[Section titled “Exit Codes”](#exit-codes)
| Code | Meaning | Action |
| ---- | --------------------- | ------------------ |
| `0` | Success, no findings | Proceed |
| `1` | General error | Check logs |
| `2` | Configuration error | Check config |
| `4` | Network error | Check connectivity |
| `5` | **Findings detected** | Review issues |
| `6` | Invalid input | Check arguments |
## Gate on Findings
[Section titled “Gate on Findings”](#gate-on-findings)
To block a pipeline when findings are detected:
```bash
unfault review --offline
if [ $? -eq 5 ]; then
echo "Findings detected. Run 'unfault review --output full' locally for details."
exit 1
fi
```
## Caching
[Section titled “Caching”](#caching)
Speed up CI runs by caching the Unfault binary:
```yaml
- name: Cache Unfault
uses: actions/cache@v4
with:
path: ~/.local/bin/unfault
key: unfault-${{ runner.os }}-latest
- name: Install Unfault
if: steps.cache.outputs.cache-hit != 'true'
run: |
mkdir -p ~/.local/bin
curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-x86_64-unknown-linux-gnu
chmod +x ~/.local/bin/unfault
```
## With Observability Enrichment
[Section titled “With Observability Enrichment”](#with-observability-enrichment)
If you want SLO/trace enrichment in CI, set the relevant credentials as secrets and drop `--offline`:
```yaml
- name: Run Unfault review
env:
# GCP: configure GOOGLE_APPLICATION_CREDENTIALS or workload identity
DD_API_KEY: ${{ secrets.DD_API_KEY }}
DD_APP_KEY: ${{ secrets.DD_APP_KEY }}
run: unfault review --output sarif > results.sarif
```
See [SLO Discovery](/docs/guides/slo-discovery/) for details on observability integrations.
# CLI Usage
> Getting started with the Unfault command-line interface.
The Unfault CLI analyzes code, explores dependencies, and queries your project’s graph from the terminal. Everything runs locally. No account, no network required.
## Installation
[Section titled “Installation”](#installation)
* Prebuilt binary (Linux x86\_64)
```bash
mkdir -p ~/.local/bin
curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-x86_64-unknown-linux-gnu
chmod +x ~/.local/bin/unfault
```
Pick the right artifact for your OS/CPU from the [releases page](https://github.com/unfault/cli/releases/latest).
* Cargo
```bash
cargo install unfault
```
* From source
```bash
git clone https://github.com/unfault/unfault
cd unfault
cargo build --release
```
## Core Commands
[Section titled “Core Commands”](#core-commands)
### Review Code
[Section titled “Review Code”](#review-code)
Analyze your codebase for production-readiness:
```bash
unfault review
```
Filter by dimension:
```bash
unfault review --dimension stability
```
Get detailed output with suggested fixes:
```bash
unfault review --output full
```
Auto-apply suggested fixes:
```bash
unfault review --fix
```
Skip SLO/trace fetching (useful in CI without observability credentials):
```bash
unfault review --offline
```
### Lint
[Section titled “Lint”](#lint)
Show all findings grouped by severity and rule, useful for a detailed linter-style view:
```bash
unfault lint
```
### File Discovery and Ignores
[Section titled “File Discovery and Ignores”](#file-discovery-and-ignores)
When Unfault scans a workspace, it respects the same ignore conventions you already use:
* `.gitignore` (including global gitignore and `.git/info/exclude`)
* `.ignore`
* `.dockerignore`
It also skips common dependency/build directories even if they aren’t explicitly ignored (e.g. `node_modules`, `target`, `dist`, `build`, `.venv`).
If Unfault reports `0` files, the usual causes are:
* running from the wrong directory
* source files are matched by one of the above ignore files
### Explore Dependencies
[Section titled “Explore Dependencies”](#explore-dependencies)
Check what depends on a file before changing it:
```bash
unfault graph impact src/api/auth.py
```
Find the most critical files in your codebase:
```bash
unfault graph critical
```
See which files use a library:
```bash
unfault graph library requests
```
Analyze what depends on a specific function:
```bash
unfault graph function-impact src/api/auth.py:validate_token
```
## Exit Codes
[Section titled “Exit Codes”](#exit-codes)
For CI/CD integration:
| Code | Meaning |
| --------- | ----------------------------------------------------------------- |
| 0 | Success, no findings |
| 5 | Findings detected |
| 1-4, 6-10 | Various errors (see [reference](/docs/reference/cli/#exit-codes)) |
Example CI usage:
```bash
unfault review
if [ $? -eq 5 ]; then
echo "Findings detected"
unfault review --output full
fi
```
## Detailed Guides
[Section titled “Detailed Guides”](#detailed-guides)
Explore the Code Graph
Understand dependencies, impact, and critical files. [Read more](/docs/guides/code-graph/)
SLO Discovery
Link cloud SLOs to your routes. [Read more](/docs/guides/slo-discovery/)
Use with AI Agents
Integrate with Claude, Cursor, and others. [Read more](/docs/guides/agents/)
CI/CD Pipeline
Add Unfault to your build process. [Read more](/docs/guides/cicd/)
## Full Reference
[Section titled “Full Reference”](#full-reference)
For complete command options, flags, and examples, see the [CLI Reference](/docs/reference/cli/).
# Explore the Code Graph
> Use the graph command to understand dependencies, impact, and critical files.
Unfault builds a graph of your codebase: which files import which, which functions call which, which libraries are used where. The `unfault graph` command lets you query this graph to understand your code’s structure and plan changes safely.
## Why This Matters
[Section titled “Why This Matters”](#why-this-matters)
Before changing code, you want to know what might break. The graph answers questions like:
* “If I refactor `auth.py`, what else needs to change?”
* “Which files use the `requests` library?”
* “What are the most interconnected files in this codebase?”
These questions are hard to answer by grepping. The graph understands imports, function calls, and transitive dependencies.
## Check What Depends on a File
[Section titled “Check What Depends on a File”](#check-what-depends-on-a-file)
Before refactoring a file, see what depends on it:
```bash
unfault graph impact src/api/auth.py
```
Output:
```plaintext
🔍 Impact Analysis: src/api/auth.py
→ This file is used by 15 file(s)
10 direct, 5 through transitive dependencies
Direct Dependencies
Files that import this directly - changes here affect them first
────────────────────────────────────────────────────────────
• src/api/routes/users.py [Python]
• src/api/routes/admin.py [Python]
• src/api/middleware/auth.py [Python]
• src/services/payments.py [Python]
...
Transitive Dependencies
Files that depend on this indirectly - ripple effects
────────────────────────────────────────────────────────────
→→ src/api/routes/orders.py [Python] (2 hops)
→→ src/api/routes/checkout.py [Python] (2 hops)
...
💡 This is a hub file. Consider extra care when modifying.
```
The direct dependents are files that import from `auth.py`. Transitive dependencies show files that depend on those dependents. Changes ripple outward.
### Control the Depth
[Section titled “Control the Depth”](#control-the-depth)
By default, transitive analysis goes 5 levels deep. For large codebases, you might want to limit this:
```bash
unfault graph impact --max-depth 2 src/core/models.py
```
Or expand it for a fuller picture:
```bash
unfault graph impact --max-depth 10 src/core/models.py
```
## Check What Depends on a Function
[Section titled “Check What Depends on a Function”](#check-what-depends-on-a-function)
Sometimes you’re changing a single function, not a whole file. The graph can be more precise:
```bash
unfault graph function-impact src/api/auth.py:validate_token
```
This shows only the files and functions that call `validate_token`, not everything that imports `auth.py`.
## Find Files Using a Library
[Section titled “Find Files Using a Library”](#find-files-using-a-library)
Auditing a dependency? See where it’s used:
```bash
unfault graph library requests
```
Output:
```plaintext
📚 Library Usage: requests
→ 'requests' is used in 7 file(s)
Usage Locations
These files import this library directly
────────────────────────────────────────────────────────────
• src/clients/payment.py [Python]
• src/clients/shipping.py [Python]
• src/scripts/migrate.py [Python]
• src/services/notifications.py [Python]
...
💡 This library is used widely. Consider its stability and versioning.
```
This is useful when:
* Migrating from one library to another (`requests` to `httpx`)
* Auditing security vulnerabilities in a dependency
* Understanding how broadly a library is used
## Find a File’s Dependencies
[Section titled “Find a File’s Dependencies”](#find-a-files-dependencies)
The inverse question: what does this file depend on?
```bash
unfault graph deps src/api/routes/payments.py
```
Output:
```plaintext
Dependencies for src/api/routes/payments.py
Internal imports (5):
← src/api/auth.py
← src/core/models.py
← src/services/stripe.py
← src/services/notifications.py
← src/utils/logging.py
External libraries (4):
← fastapi
← sqlalchemy
← pydantic
← structlog
```
This helps when:
* Understanding a file before modifying it
* Checking if a file has too many dependencies
* Auditing what a module actually needs
## Find Critical Files
[Section titled “Find Critical Files”](#find-critical-files)
Every codebase has hub files: highly connected, imported by many others. These are your most critical files. Changes to them have wide impact.
```bash
unfault graph critical
```
Output:
```plaintext
🎯 Hub Files Analysis
Files with the most connections - changes here have the widest impact
→ Analyzing 185 files, showing top 10
# File In Out Libs Score
─────────────────────────────────────────────────────────────────
1 src/__init__.py 56 0 0 112
2 src/database.py 47 1 11 106
3 src/dependencies/database.py 28 1 2 59
4 src/config.py 27 0 4 58
5 src/schemas.py 19 0 3 41
...
💡 '__init__.py' is a major hub. Consider extra review for changes.
Legend: In: dependents | Out: dependencies | Libs: external packages
Higher score = more central to the codebase
```
These are files to handle with care. Changes here ripple widely.
### Different Sorting Criteria
[Section titled “Different Sorting Criteria”](#different-sorting-criteria)
You can sort by different metrics:
```bash
# Files most imported by others (default)
unfault graph critical --sort-by in-degree
# Files that import the most (sprawling dependencies)
unfault graph critical --sort-by out-degree
# Total connectivity (hubs)
unfault graph critical --sort-by total-degree
# Most external library usage
unfault graph critical --sort-by library-usage
```
### More or Fewer Results
[Section titled “More or Fewer Results”](#more-or-fewer-results)
```bash
# Top 20 critical files
unfault graph critical --limit 20
# Just the top 3
unfault graph critical --limit 3
```
## Get Graph Statistics
[Section titled “Get Graph Statistics”](#get-graph-statistics)
For a high-level view of your codebase structure:
```bash
unfault graph stats
```
Output:
```plaintext
🗺️ Code Graph Overview
A map of your codebase structure and connections
→ 723 code units across 185 files
Structure
─────────────────────────────────────────────
📄 Files 185
⚙️ Functions 723
📦 Classes 0
📚 External Libraries 69
─────────────────────────────────────────────
Total nodes 978
Connections
─────────────────────────────────────────────
🔗 File imports 409
📍 File→function/class 731
📚 Library usage 656
➡️ Function calls 628
─────────────────────────────────────────────
Total edges 2680
```
## JSON Output
[Section titled “JSON Output”](#json-output)
All graph commands support `--json` for programmatic use:
```bash
unfault graph impact --json src/api/auth.py
unfault graph critical --json --limit 5
```
This is useful for:
* CI/CD checks (“fail if this file has more than N dependents”)
* Custom tooling
* Integration with other systems
## Practical Workflows
[Section titled “Practical Workflows”](#practical-workflows)
### Before a Refactor
[Section titled “Before a Refactor”](#before-a-refactor)
1. Check what depends on the file you’re changing:
```bash
unfault graph impact src/core/models.py
```
2. If the impact is large, consider whether to:
* Break the change into smaller pieces
* Add deprecation warnings first
* Update dependents in the same PR
3. After making changes, run a review to catch issues:
```bash
unfault review
```
### Auditing a Dependency
[Section titled “Auditing a Dependency”](#auditing-a-dependency)
1. Find all usage of the library:
```bash
unfault graph library requests
```
2. Check each file for patterns (timeouts, retries):
```bash
unfault review --dimension stability
```
3. If migrating, you now have a complete list of files to update.
### Understanding a New Codebase
[Section titled “Understanding a New Codebase”](#understanding-a-new-codebase)
1. Get the overall shape:
```bash
unfault graph stats
```
2. Find the critical files (these are the “load-bearing walls”):
```bash
unfault graph critical --limit 10
```
3. Pick a critical file and see what depends on it:
```bash
unfault graph impact src/core/models.py
```
## Next Steps
[Section titled “Next Steps”](#next-steps)
CLI Reference
Full option reference for graph commands. [Read more](/docs/reference/cli/#graph)
Query the Codebase
Structural queries for exploration and understanding. [Read more](/docs/guides/asking-questions/)
# Adding External Integrations
> Write reliable code when connecting to external services.
You’re adding code that calls an external API, a third-party service, or a database. These integrations are where production incidents often start. A missing timeout, an unbounded retry loop, or no error handling can take down your service when the external dependency misbehaves.
This guide shows how to use Unfault to catch these patterns before they ship.
## The Patterns That Cause Incidents
[Section titled “The Patterns That Cause Incidents”](#the-patterns-that-cause-incidents)
When code calls external services, a few patterns cause most of the problems:
**Missing timeouts**: The external service hangs. Your request hangs. Your thread pool fills up. Your service stops responding.
**Unbounded retries**: The external service returns errors. Your code retries forever. You amplify the problem and possibly get rate limited.
**No circuit breaker**: The external service is down. Every request tries to reach it, fails, and takes the slow timeout path. Your service becomes unusably slow.
**Swallowed errors**: Something fails. The error gets caught and ignored. Debugging becomes archaeology.
**No correlation IDs**: A request fails somewhere in the chain. You can’t trace what happened across services.
Unfault detects all of these.
## Reviewing Integration Code
[Section titled “Reviewing Integration Code”](#reviewing-integration-code)
After writing code that connects to an external service:
```bash
unfault review --dimension stability
```
The `--dimension stability` flag focuses on the patterns most relevant to external integrations.
Example output:
```plaintext
High: Missing HTTP timeout
src/integrations/billing.py:42
HTTP request without timeout can hang indefinitely
High: Missing circuit breaker
src/integrations/billing.py:38
External service call without circuit breaker
Prerequisites: request classification, metrics collection
Medium: No retry logic
src/integrations/billing.py:42
HTTP call may fail transiently without retry
```
## Making Decisions
[Section titled “Making Decisions”](#making-decisions)
Not every finding requires immediate action. Here’s how to think about them:
### Timeouts
[Section titled “Timeouts”](#timeouts)
Almost always worth adding. This is a low-effort, high-impact fix:
```python
# Before
response = requests.get(url)
# After
response = requests.get(url, timeout=30.0)
```
### Retries
[Section titled “Retries”](#retries)
Worth adding for idempotent operations. Be careful with non-idempotent ones:
```python
# Safe for GET requests
@retry(stop=stop_after_attempt(3), wait=wait_exponential())
def fetch_data():
return requests.get(url, timeout=30.0)
# Be careful with POST requests that aren't idempotent
```
### Circuit Breakers
[Section titled “Circuit Breakers”](#circuit-breakers)
These require more infrastructure. If Unfault reports prerequisites are missing (metrics, request classification), this is a note for future work, not something to implement in this PR.
### Error Handling
[Section titled “Error Handling”](#error-handling)
If Unfault reports empty catch blocks or swallowed exceptions, add appropriate handling:
```python
# Before
try:
response = client.fetch()
except Exception:
pass # Empty catch
# After
try:
response = client.fetch()
except RequestException as e:
logger.error("Failed to fetch from billing service", error=str(e))
raise BillingServiceError("Could not reach billing service") from e
```
## A Practical Workflow
[Section titled “A Practical Workflow”](#a-practical-workflow)
1. **Write the integration code**
Get it working first.
2. **Review for stability patterns**
```bash
unfault review --dimension stability
```
3. **Add timeouts**
These are almost always worth adding.
4. **Add error handling**
Make sure errors are logged and propagated appropriately.
5. **Consider retries**
For idempotent operations on flaky services.
6. **Note infrastructure gaps**
If Unfault suggests circuit breakers or rate limiting but prerequisites are missing, document this for future work.
7. **Review again**
```bash
unfault review --dimension stability
```
Confirm the high-severity findings are addressed.
## With AI Agents
[Section titled “With AI Agents”](#with-ai-agents)
When an AI assistant writes integration code, it often forgets these operational concerns. Add to your AGENTS.md:
```text
When writing code that calls external services:
1. Always include timeouts on HTTP calls
2. Consider retry logic for idempotent operations
3. Log errors with enough context to debug later
4. After writing, run: unfault review --dimension stability
5. Address high-severity findings before committing
```
See [Use with AI Agents](/docs/guides/agents/) for full setup instructions.
## Checking Existing Patterns
[Section titled “Checking Existing Patterns”](#checking-existing-patterns)
Before writing new integration code, see how similar integrations work in your codebase. Find files that already use HTTP clients:
```bash
unfault graph library requests
unfault graph library httpx
```
This helps you match existing conventions rather than inventing new patterns.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Pre-commit Review
Review all changes before committing. [Read more](/docs/guides/pre-commit-review/)
Fault Injection
Test how your code handles failures. [Read more](/docs/guides/fault/)
Rules Catalog
Browse stability rules in detail. [Read more](/docs/reference/rules/)
# Fault Injection
> Use fault to exercise timeouts, latency, jitter, and outages against your app.
Unfault’s LSP and VS Code extension can integrate with [fault](https://fault-project.com/) to help you run targeted fault injections against your app while you’re looking at a specific route or function.
The goal is not to build a perfect chaos suite on day one. It’s to answer: “What happens if this upstream is slow, flaky, or down?” while you’re still in the code.
## Install the fault CLI
[Section titled “Install the fault CLI”](#install-the-fault-cli)
Install `fault` from its GitHub releases or directly:
```bash
# macOS
brew install fault-project/tap/fault
# Linux / manual install
curl -Lo fault https://github.com/fault-project/fault/releases/latest/download/fault-linux-x86_64
chmod +x fault && mv fault ~/.local/bin/
```
## Configure the App Base URL
[Section titled “Configure the App Base URL”](#configure-the-app-base-url)
The VS Code extension needs to know where your app is listening. Set in your VS Code settings:
* `unfault.fault.baseUrl` (default: `http://127.0.0.1:8000`)
This is the target Unfault maps to the local fault proxy.
## Run a Fault Injection from VS Code
[Section titled “Run a Fault Injection from VS Code”](#run-a-fault-injection-from-vs-code)
When your cursor is inside a route handler (or a function reached by one), open the **Unfault: Context** sidebar and use the **Fault Injection** panel.
When you click **Run**:
* Unfault starts a streaming proxy with `fault run --proxy "9090=" ...`.
* Unfault opens a split terminal with a ready-to-edit `curl http://127.0.0.1:9090/` command.
* The curl command is not executed automatically.
This makes the architecture explicit: your client talks to the fault proxy, and the proxy streams traffic to your app.
Note
In streaming proxy mode, Unfault doesn’t offer HTTP error injection templates (the stream is opaque). Stick to network-style faults: latency, jitter, bandwidth, packet loss, and blackhole.
## Generate Scenario Files
[Section titled “Generate Scenario Files”](#generate-scenario-files)
If you want repeatable checks, use the **Generate scenario file** button in the same panel.
Unfault generates one scenario suite per discovered route and saves it under:
* `tests/fault/` if it exists
* otherwise `test/fault/` if it exists
* otherwise it creates `tests/fault/`
Each file contains multiple YAML documents separated by `---` (a small suite of scenarios for that route).
To run a scenario file:
```bash
fault scenario run --scenario tests/fault/post-payments.yaml
```
## Running Fault Injections
[Section titled “Running Fault Injections”](#running-fault-injections)
Use `fault run` directly for quick, targeted injections:
```bash
# Add 200ms latency for 30 seconds, proxying to your app
fault run --proxy "9090=http://127.0.0.1:8000" --latency 200ms --duration 30s
# Simulate a complete blackhole (service unreachable)
fault run --proxy "9090=http://127.0.0.1:8000" --blackhole --duration 15s
```
Then drive traffic through the proxy port (9090) to observe how your code handles it.
Note
The fault integration in Unfault focuses on a small, practical subset of fault scenarios. For advanced scenarios, use `fault` directly.
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
**fault not found**
* The VS Code extension will prompt if `fault` isn’t installed.
* You can also set `unfault.fault.executablePath` if `fault` is not on your `PATH`.
**Port 9090 already in use**
* The fault proxy binds to `127.0.0.1:9090`.
* If you rerun with a different template, Unfault stops the previous proxy before starting a new one.
# Monorepo Setup
> How to configure Unfault for monorepo projects.
Monorepos bring multiple services, libraries, and applications into a single repository. Unfault handles this by analyzing from whatever directory you run it in.
## Basic Usage
[Section titled “Basic Usage”](#basic-usage)
Unfault analyzes from the current directory down:
```bash
# Analyze everything from repo root
cd my-monorepo
unfault review
# Analyze just the payments service
cd services/payments
unfault review
```
This is the primary workflow. Navigate to what you want to analyze, then run the review.
## Workspace Configuration
[Section titled “Workspace Configuration”](#workspace-configuration)
Each service in a monorepo can have its own configuration. Unfault looks for config in these locations:
1. Current directory’s manifest file (`pyproject.toml`, `Cargo.toml`, `package.json`)
2. Parent directories up to the repo root
3. `unfault.toml` if no manifest exists
### Per-Service Configuration
[Section titled “Per-Service Configuration”](#per-service-configuration)
```plaintext
my-monorepo/
├── services/
│ ├── payments/
│ │ ├── pyproject.toml # Payments-specific config
│ │ └── src/
│ └── users/
│ ├── pyproject.toml # Users-specific config
│ └── src/
└── pyproject.toml # Root config (optional)
```
Each `pyproject.toml` can have its own `[tool.unfault]` section:
services/payments/pyproject.toml
```toml
[tool.unfault]
dimensions = ["stability", "security"] # Payments cares most about these
[tool.unfault.rules]
exclude = ["python.http.missing_circuit_breaker"] # Handled at gateway
```
### Shared Configuration
[Section titled “Shared Configuration”](#shared-configuration)
For settings that apply everywhere, put them in the root config:
```toml
# pyproject.toml (root)
[tool.unfault]
dimensions = ["stability", "correctness", "performance"]
[tool.unfault.rules]
exclude = [
"python.missing_structured_logging:scripts/*", # Scripts everywhere
]
```
Child directories inherit from parent configs and can override specific settings.
## Multi-Language Monorepos
[Section titled “Multi-Language Monorepos”](#multi-language-monorepos)
Monorepos often contain multiple languages. Unfault detects and analyzes each:
```plaintext
my-monorepo/
├── services/
│ ├── api/ # Python (FastAPI)
│ ├── worker/ # Go
│ └── gateway/ # Rust
├── frontend/ # TypeScript
└── scripts/ # Python
```
```bash
# Analyze everything, all languages
cd my-monorepo
unfault review
# Just the backend services
cd services
unfault review
# Just the API
cd services/api
unfault review
```
## CI/CD for Monorepos
[Section titled “CI/CD for Monorepos”](#cicd-for-monorepos)
### Analyze Changed Services Only
[Section titled “Analyze Changed Services Only”](#analyze-changed-services-only)
.github/workflows/unfault.yml
```yaml
name: Unfault Review
on:
pull_request:
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed services
id: changed
run: |
# Get unique service directories that changed
SERVICES=$(git diff --name-only origin/main | grep -E '^services/' | cut -d/ -f1-2 | sort -u)
echo "services=$SERVICES" >> $GITHUB_OUTPUT
- name: Install Unfault
run: |
mkdir -p ~/.local/bin
curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-x86_64-unknown-linux-gnu
chmod +x ~/.local/bin/unfault
- name: Review changed services
if: steps.changed.outputs.services != ''
run: |
for service in ${{ steps.changed.outputs.services }}; do
echo "Reviewing $service..."
cd $service
unfault review
cd -
done
```
### Parallel Analysis
[Section titled “Parallel Analysis”](#parallel-analysis)
For large monorepos, analyze services in parallel using a matrix:
```yaml
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
services: ${{ steps.detect.outputs.services }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: detect
run: |
SERVICES=$(git diff --name-only origin/main | grep -E '^services/' | cut -d/ -f1-2 | sort -u | jq -R -s -c 'split("\n") | map(select(. != ""))')
echo "services=$SERVICES" >> $GITHUB_OUTPUT
review:
needs: detect-changes
if: needs.detect-changes.outputs.services != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
service: ${{ fromJson(needs.detect-changes.outputs.services) }}
steps:
- uses: actions/checkout@v4
- run: |
mkdir -p ~/.local/bin
curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-x86_64-unknown-linux-gnu
chmod +x ~/.local/bin/unfault
- name: Review service
working-directory: ${{ matrix.service }}
run: unfault review
```
## Graph Across Services
[Section titled “Graph Across Services”](#graph-across-services)
The code graph understands cross-service relationships when imports or calls cross boundaries:
```bash
# What depends on a file in the shared library?
unfault graph impact libs/common/utils.py
# Find external dependencies of the API service
cd services/api
unfault graph deps src/main.py
```
This helps understand how changes propagate across service boundaries.
## Common Patterns
[Section titled “Common Patterns”](#common-patterns)
### Shared Libraries
[Section titled “Shared Libraries”](#shared-libraries)
```plaintext
my-monorepo/
├── libs/
│ └── common/ # Shared utilities
└── services/
├── api/ # Uses libs/common
└── worker/ # Uses libs/common
```
Analyze the library to see impact across consumers:
```bash
unfault graph impact libs/common/core.py
```
### Different Rules per Service Type
[Section titled “Different Rules per Service Type”](#different-rules-per-service-type)
services/public-api/pyproject.toml
```toml
[tool.unfault]
dimensions = ["security", "stability", "performance"]
# services/internal-worker/pyproject.toml
[tool.unfault]
dimensions = ["correctness", "stability"]
[tool.unfault.rules]
exclude = ["python.http.*"] # Worker doesn't expose HTTP
```
### Excluding Non-Production Code
[Section titled “Excluding Non-Production Code”](#excluding-non-production-code)
```toml
# Root pyproject.toml
[tool.unfault.rules]
exclude = [
"*:scripts/*",
"*:tools/*",
"*:examples/*",
"*:**/tests/*",
]
```
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
### ”Analysis takes too long”
[Section titled “”Analysis takes too long””](#analysis-takes-too-long)
Narrow the scope by running from a subdirectory:
```bash
# Instead of the whole monorepo
cd my-monorepo
unfault review
# Analyze just what you're working on
cd services/api
unfault review
```
### “Different findings in CI vs local”
[Section titled ““Different findings in CI vs local””](#different-findings-in-ci-vs-local)
Make sure you’re running from the same directory in both environments. The working directory determines what gets analyzed.
### ”Config not being picked up”
[Section titled “”Config not being picked up””](#config-not-being-picked-up)
Use verbose mode to see what’s happening:
```bash
unfault review -v
```
This shows which config files are being loaded and what settings are applied.
## Next Steps
[Section titled “Next Steps”](#next-steps)
CI/CD Integration
Full CI/CD setup guide. [Read more](/docs/guides/cicd/)
Configuration
All configuration options. [Read more](/docs/reference/configuration/)
Suppressing Rules
Customize per-service rules. [Read more](/docs/guides/suppressing-rules/)
# Copy/Paste Oneliners
> Ready-to-run commands for common scenarios.
No explanation needed. Just copy, paste, and run.
## Before You Commit
[Section titled “Before You Commit”](#before-you-commit)
```bash
# Summary review: header + findings narrative
unfault review
# All findings grouped by severity and rule (linter view)
unfault lint
# Detailed output with suggested fixes
unfault review --output full
# Machine-readable JSON
unfault review --output json
# Focus on stability only
unfault review --dimension stability
```
## For Coding Agents
[Section titled “For Coding Agents”](#for-coding-agents)
Structured output for Claude, Cursor, Copilot, and other AI assistants.
```bash
# JSON output for programmatic parsing
unfault review --output json
# Focus on specific dimensions
unfault review --dimension stability --output json
unfault review --dimension correctness --output json
# Impact analysis before changes
unfault graph impact src/api/routes.py --json
unfault graph critical --json --limit 5
```
Note
The `--output json` format includes `file_path`, `line`, `severity`, `title`, and `fix_preview` for each finding. See the [Output Reference](/docs/reference/api/) for the full schema.
### Paste Into Your Agent
[Section titled “Paste Into Your Agent”](#paste-into-your-agent)
Add this to your `AGENTS.md` or system prompt:
```markdown
Before committing, run:
\`\`\`bash
unfault review --output json
\`\`\`
If high-severity findings are reported, address them before committing.
Before changing a file, check its blast radius:
\`\`\`bash
unfault graph impact
\`\`\`
```
## CI/CD Pipelines
[Section titled “CI/CD Pipelines”](#cicd-pipelines)
### GitHub Actions
[Section titled “GitHub Actions”](#github-actions)
```bash
# SARIF for GitHub Code Scanning
unfault review --output sarif > results.sarif
```
.github/workflows/unfault.yml
```yaml
- name: Run Unfault
run: unfault review --output sarif > results.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
```
### Exit Code Checks
[Section titled “Exit Code Checks”](#exit-code-checks)
```bash
# Fail if findings detected (exit 5)
unfault review && echo "Clean!" || echo "Findings found"
# Explicit check
unfault review --output json
if [ $? -eq 5 ]; then
echo "Review the findings above"
exit 1
fi
```
### JSON for Custom Processing
[Section titled “JSON for Custom Processing”](#json-for-custom-processing)
```bash
# Pipe to jq for filtering
unfault review --output json | jq '.contexts[].findings | map(select(.severity == "High"))'
# Count findings
unfault review --output json | jq '[.contexts[].findings] | flatten | length'
```
## Deep Analysis
[Section titled “Deep Analysis”](#deep-analysis)
```bash
# Full detailed output with suggested fixes
unfault review --output full
# Focus on one dimension
unfault review --dimension stability --output full
unfault review --dimension performance --output full
unfault review --dimension correctness --output full
# Preview fixes without applying
unfault review --dry-run
```
## Impact Analysis
[Section titled “Impact Analysis”](#impact-analysis)
```bash
# What breaks if I change this file?
unfault graph impact src/core/auth.py
# Find the most connected files
unfault graph critical --limit 10
# What uses this library?
unfault graph library requests
unfault graph library httpx
# External dependencies of a file
unfault graph deps src/api/client.py
# Graph statistics
unfault graph stats
```
## Configuration
[Section titled “Configuration”](#configuration)
```bash
# Configure LLM for AI-powered review summaries
unfault config llm openai --model gpt-4o
unfault config llm anthropic --model claude-3-5-sonnet-latest
unfault config llm ollama --model llama3.2
# View current config
unfault config show
unfault config llm show
# Check observability integrations
unfault config integrations show
unfault config integrations verify
```
## Quick Reference
[Section titled “Quick Reference”](#quick-reference)
| Flag | What it does |
| ------------------ | -------------------------------------- |
| `--output full` | Detailed output with fix suggestions |
| `--output json` | Machine-readable JSON |
| `--output sarif` | SARIF 2.1.0 for GitHub/IDE integration |
| `--output concise` | Brief statistics only |
| `--dimension X` | Focus on one dimension |
| `--dry-run` | Preview fixes without applying |
| `--fix` | Auto-apply suggested fixes |
| `--offline` | Skip SLO and trace fetching |
| `--refresh-cache` | Re-fetch observability data |
## Exit Codes
[Section titled “Exit Codes”](#exit-codes)
| Code | Meaning |
| ---- | -------------------------- |
| 0 | Success, no findings |
| 5 | Success, findings detected |
| 1 | General error |
| 2 | Config error |
| 4 | Network error |
| 6 | Invalid input |
AI Agents Guide
Full integration patterns. [Read more](/docs/guides/agents/)
CI/CD Guide
Pipeline integration details. [Read more](/docs/guides/cicd/)
CLI Reference
Complete command docs. [Read more](/docs/reference/cli/)
# Pre-commit Code Review
> Review code for production readiness before you commit.
You’ve written code. Before you commit, you want to know if it has patterns that cause problems in production. This guide shows you how to use Unfault for that check.
## The Quick Version
[Section titled “The Quick Version”](#the-quick-version)
```bash
unfault review
```
For detailed output with suggested fixes:
```bash
unfault review --output full
```
## What Unfault Looks For
[Section titled “What Unfault Looks For”](#what-unfault-looks-for)
Unfault doesn’t check syntax or style. It looks for operational patterns:
* **Missing safeguards**: HTTP calls without timeouts, retries without limits, caches without bounds
* **Error handling gaps**: Empty catch blocks, swallowed exceptions, missing error propagation
* **Resource issues**: Unbounded concurrency, connection leaks, blocking calls in async code
* **Observability gaps**: Missing correlation IDs, no structured logging, absent tracing
These are the patterns that work fine in development and cause incidents in production.
## Reading the Output
[Section titled “Reading the Output”](#reading-the-output)
The default output gives you a summary:
```plaintext
Looks good overall, with a couple spots that deserve a closer look.
At a glance
· 2 calls without timeouts - could hang if a service is slow
· 1 empty catch block - errors might vanish silently
────────────────────────────────────────────────────────────────────────────────
847ms - python / fastapi - 3 files
Tip: use --output full to drill into hotspots.
```
Use `--output full` to see exactly where the issues are and what to do about them:
```bash
unfault review --output full
```
This shows each finding with:
* File and line number
* What was detected
* Why it matters
* A suggested fix (when applicable)
## Focusing Your Review
[Section titled “Focusing Your Review”](#focusing-your-review)
### By Dimension
[Section titled “By Dimension”](#by-dimension)
If you’re working on a specific concern:
```bash
# Only stability issues (timeouts, retries, error handling)
unfault review --dimension stability
# Only performance issues (blocking calls, resource usage)
unfault review --dimension performance
# Multiple dimensions
unfault review --dimension stability --dimension correctness
```
### By Severity
[Section titled “By Severity”](#by-severity)
When there are many findings, focus on high severity first. The summary groups findings by severity, and `--output full` shows severity for each finding.
## Making Decisions
[Section titled “Making Decisions”](#making-decisions)
Not every finding needs action. Here’s a framework:
1. **High severity in code you just wrote**
Worth addressing before commit. These are patterns with real production impact.
2. **Medium severity in code you wrote**
Consider the context. If the code is on a critical path, address it. If it’s a script or tool, maybe not.
3. **Low severity**
Informational. Good to know, rarely blocking.
4. **Findings in files you didn’t change**
Existing patterns. Note them if relevant, but they’re not your responsibility in this PR.
## Integrating with Your Workflow
[Section titled “Integrating with Your Workflow”](#integrating-with-your-workflow)
### Git Hook
[Section titled “Git Hook”](#git-hook)
Add a pre-commit hook that runs Unfault:
.git/hooks/pre-commit
```bash
#!/bin/sh
unfault review --output concise --offline
EXIT_CODE=$?
if [ $EXIT_CODE -eq 5 ]; then
echo ""
echo "Unfault found patterns worth reviewing."
echo "Run 'unfault review --output full' for details."
echo "Commit anyway with --no-verify if this is intentional."
exit 1
fi
exit 0
```
Make it executable:
```bash
chmod +x .git/hooks/pre-commit
```
### With AI Agents
[Section titled “With AI Agents”](#with-ai-agents)
If you’re using an AI coding assistant, add to your `AGENTS.md`:
```text
Before committing changes that involve external services, error handling,
or resource management, run:
unfault review --dimension stability --output json
Address high-severity findings before committing.
```
See [Use with AI Agents](/docs/guides/agents/) for more detail.
See [Use with AI Agents](/docs/guides/agents/) for more detail.
## Common Patterns
[Section titled “Common Patterns”](#common-patterns)
### ”I know this is fine”
[Section titled “”I know this is fine””](#i-know-this-is-fine)
Sometimes Unfault flags something that’s intentional. A few options:
1. **Add a suppression comment** (see [Suppressing Rules](/docs/guides/suppressing-rules/))
2. **Just commit.** Unfault is advisory, not mandatory.
### ”There are too many findings”
[Section titled “”There are too many findings””](#there-are-too-many-findings)
If Unfault reports many findings:
```bash
# Focus on one dimension at a time
unfault review --dimension stability
```
The goal is actionable feedback, not comprehensive audits.
### ”I want to fix everything”
[Section titled “”I want to fix everything””](#i-want-to-fix-everything)
Resist the urge. Pre-commit review is for catching issues in *your changes*. Broader cleanup belongs in dedicated refactoring work.
## Next Steps
[Section titled “Next Steps”](#next-steps)
CI/CD Integration
Automate reviews in your pipeline. [Read more](/docs/guides/cicd/)
Configuration
Customize rules and thresholds. [Read more](/docs/reference/configuration/)
Rules Catalog
Browse what Unfault detects. [Read more](/docs/reference/rules/)
# SLO Discovery
> Link your cloud SLOs to your codebase for route-level observability.
Unfault can discover Service Level Objectives (SLOs) from your cloud provider and link them to the route handlers in your codebase. This gives you visibility into which routes are covered by SLOs and surfaces gaps in observability.
## Why This Matters
[Section titled “Why This Matters”](#why-this-matters)
SLOs define what “healthy” means for your service. But SLOs are typically configured at the service level in your cloud provider’s console, disconnected from the code that actually handles requests.
When an SLO breach happens, you need to trace from the alert back to the code. Unfault bridges this gap by linking SLOs directly to your route handlers.
## Discovery
[Section titled “Discovery”](#discovery)
SLO discovery runs automatically when Unfault detects valid cloud credentials. Just run a review:
```bash
unfault review
```
When credentials are present, Unfault queries your cloud project for existing SLOs and presents them interactively:
```plaintext
We found SLOs in your GCP project that apply to entire services.
Which service does this codebase deploy to?
[1] unfault-prod-cloudrun-api-slo
└─ 100% Availability - 99.9% over 30d
└─ 99% Error Rate - <1% over 30d
└─ 99% Latency - 95th < 500ms over 7d
[s] Skip (don't link SLOs)
> 1
```
Select the service that matches your codebase. Unfault links the SLOs to your route handlers:
```plaintext
✓ Linked 3 SLO(s) to 375 route handler(s).
(Saved to config, won't ask again for this workspace)
```
This mapping is saved locally. Future reviews remember the link without prompting again.
## Review Output with SLOs
[Section titled “Review Output with SLOs”](#review-output-with-slos)
Once linked, your review output includes observability coverage:
```plaintext
→ Analyzing backend... 7215ms
Languages: python
Frameworks: fastapi
Dimensions: stability · correctness · performance
Reviewed: 185 files · parse 81ms · engine 1551ms
Cache: 100% Trace: 52924be6
Summary
Looks good overall, with a couple spots that deserve a closer look. Two themes
keep showing up: resilience hardening and other cleanup. Starting point:
scripts/create-plan.py (HTTP call to external service in `run` lacks circuit
breaker protec...); then scripts/trigger-throttle.py (HTTP call to external
service in `run` lacks circuit breaker protec...).
────────────────────────────────────────────────────────────
📊 Observability: 3 SLO(s) linked to 125/125 routes (100% coverage)
✓ All your HTTP routes are covered by SLOs.
This gives you visibility into how users are experiencing your service.
Tip: use --output full to drill into hotspots.
```
The observability section shows:
* How many SLOs are linked
* How many routes are covered
* Coverage percentage
* Whether any routes lack observability
## Supported Providers
[Section titled “Supported Providers”](#supported-providers)
Currently supported:
| Provider | SLO Source |
| ---------------- | --------------------- |
| **Google Cloud** | Cloud Monitoring SLOs |
Note
Support for AWS CloudWatch SLOs and Datadog SLOs is planned.
## Authentication
[Section titled “Authentication”](#authentication)
SLO discovery requires authentication with your cloud provider. Unfault uses your existing credentials:
**Google Cloud**: Application Default Credentials (ADC)
```bash
gcloud auth application-default login
```
If credentials aren’t available, Unfault skips discovery and continues with the review.
## Configuration
[Section titled “Configuration”](#configuration)
After the initial link, the SLO mapping is stored in your workspace. You can view and modify it:
```bash
unfault config show
```
To re-run discovery (useful if you’ve added new SLOs), force a cache refresh:
```bash
unfault review --refresh-cache
```
To unlink SLOs and start fresh, remove the mapping from `.unfault/cache/enrichment/`.
## What Gets Linked
[Section titled “What Gets Linked”](#what-gets-linked)
Unfault links SLOs to:
* **FastAPI routes** (`@app.get`, `@app.post`, etc.)
* **Flask routes** (`@app.route`)
* **Express routes** (`app.get`, `router.post`, etc.)
* **Go HTTP handlers** (`http.HandleFunc`, gorilla/mux, chi)
The link is semantic, not syntactic. Unfault understands your routing structure and maps SLOs to the handlers that serve traffic.
## Coverage Gaps
[Section titled “Coverage Gaps”](#coverage-gaps)
If some routes aren’t covered by SLOs, Unfault tells you:
```plaintext
📊 Observability: 2 SLO(s) linked to 118/125 routes (94% coverage)
⚠ 7 routes have no SLO coverage:
→ POST /internal/sync
→ GET /internal/health
→ POST /webhooks/stripe
...
Consider whether these routes need observability.
```
Not every route needs an SLO. Internal health checks and webhook endpoints might be fine without one. The report helps you make that decision consciously.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Dimensions
Understand the Observability dimension. [Read more](/docs/concepts/dimensions/)
CI/CD Integration
Track SLO coverage over time. [Read more](/docs/guides/cicd/)
# Suppressing Rules
> How to customize which rules Unfault applies and when.
Not every finding needs action. Sometimes a pattern that’s generally problematic is fine in your specific context. This guide explains how to suppress rules when they don’t apply.
## When to Suppress
[Section titled “When to Suppress”](#when-to-suppress)
Suppress a rule when:
* **The context makes it safe.** An HTTP call to localhost doesn’t need the same timeout as one to an external service.
* **You have safeguards elsewhere.** Maybe retry logic exists at a different layer.
* **It’s intentional.** You’ve considered the trade-off and accepted the risk.
* **It’s a false positive.** The rule doesn’t understand your specific pattern.
Don’t suppress just to make findings go away. Each suppression should have a reason you could explain to a teammate.
## Inline Suppression
[Section titled “Inline Suppression”](#inline-suppression)
Suppress a specific finding with a comment on the line above:
* Python
ignore\[python.http.missing\_timeout]
```python
response = requests.get(internal_url) # Internal service, <1ms latency
```
* Go
ignore\[go.http.missing\_timeout]
```go
resp, err := http.Get(internalURL) // Internal service, <1ms latency
```
* Rust
ignore\[rust.http.missing\_timeout]
```rust
let response = client.get(internal_url).send()?; // Internal service
```
* TypeScript
```typescript
// unfault: ignore[typescript.http.missing_timeout]
const response = await fetch(internalUrl); // Internal service
```
### Multiple Rules
[Section titled “Multiple Rules”](#multiple-rules)
Suppress multiple rules on the same line:
```python
# unfault: ignore[python.http.missing_timeout, python.http.missing_retry]
response = requests.get(cache_url) # Local cache, fast and reliable
```
### Adding Context
[Section titled “Adding Context”](#adding-context)
Add a reason after the rule list (recommended):
```python
# unfault: ignore[python.http.missing_timeout] -- localhost cache, sub-ms response
response = requests.get("http://localhost:6379/health")
```
The text after `--` is ignored by Unfault but helps humans understand why the suppression exists.
## File-Level Suppression
[Section titled “File-Level Suppression”](#file-level-suppression)
Suppress rules for an entire file by adding the comment at the top:
ignore-file\[python.missing\_structured\_logging]
```python
# This is a CLI script, not a service. Structured logging doesn't apply.
import click
@click.command()
def main():
print("Running migration...")
```
This is useful for:
* Scripts that aren’t production services
* Test files with intentionally problematic patterns
* Generated code
## Project-Level Suppression
[Section titled “Project-Level Suppression”](#project-level-suppression)
Suppress rules across your entire project in your manifest file:
* Python
pyproject.toml
```toml
[tool.unfault.rules]
exclude = [
"python.missing_structured_logging", # We use custom logging
"python.http.missing_circuit_breaker", # Circuit breakers at gateway
]
```
* Go
unfault.toml
```toml
[rules]
exclude = [
"go.missing_structured_logging",
"go.http.missing_circuit_breaker",
]
```
* Rust
Cargo.toml
```toml
[package.metadata.unfault.rules]
exclude = [
"rust.missing_structured_logging",
"rust.http.missing_circuit_breaker",
]
```
* TypeScript
package.json
```json
{
"unfault": {
"rules": {
"exclude": [
"typescript.missing_structured_logging",
"typescript.http.missing_circuit_breaker"
]
}
}
}
```
### Path-Based Exclusions
[Section titled “Path-Based Exclusions”](#path-based-exclusions)
Exclude rules for specific paths:
pyproject.toml
```toml
[tool.unfault.rules]
exclude = [
"python.http.*:scripts/*", # All HTTP rules in scripts/
"python.missing_timeout:tests/*", # Timeouts in tests
]
```
The pattern is `rule:path-glob`.
## Severity Overrides
[Section titled “Severity Overrides”](#severity-overrides)
Instead of suppressing entirely, you can lower a rule’s severity:
pyproject.toml
```toml
[tool.unfault.rules.severity]
"python.bare_except" = "low" # Demote to low
"python.http.missing_retry" = "medium" # Demote from high to medium
```
This keeps the finding visible but changes how it’s prioritized.
Tip
Severity overrides are good for rules you want to track but not gate on. The finding still appears; it just doesn’t block CI.
## Checking Suppressions
[Section titled “Checking Suppressions”](#checking-suppressions)
To see what’s being suppressed, review your configuration files and inline comments. Suppressions are documented where they’re defined:
* **Inline**: Search for `unfault: ignore` in your code
* **File-level**: Look for `unfault: ignore-file` at the top of files
* **Project-level**: Check the `exclude` array in your `pyproject.toml`, `Cargo.toml`, or `package.json`
Running `unfault review --output full` shows the rules that matched, making it easier to understand what’s being checked.
## Common Suppression Patterns
[Section titled “Common Suppression Patterns”](#common-suppression-patterns)
### Internal Services
[Section titled “Internal Services”](#internal-services)
```python
# unfault: ignore[python.http.missing_timeout] -- internal service mesh, handled by sidecar
response = requests.get(f"http://user-service/users/{user_id}")
```
### Test Code
[Section titled “Test Code”](#test-code)
```toml
[tool.unfault.rules]
exclude = [
"*:tests/*", # Exclude all rules in tests/
"*:*_test.py", # Exclude all rules in test files
]
```
### Generated Code
[Section titled “Generated Code”](#generated-code)
```python
# unfault: ignore-file[*]
# AUTO-GENERATED FILE - DO NOT EDIT
# Generated by protoc-gen-python
```
### Legacy Code
[Section titled “Legacy Code”](#legacy-code)
pyproject.toml
```toml
[tool.unfault.rules]
exclude = [
"*:legacy/*", # Legacy code, being migrated
]
```
Caution
Blanket exclusions for legacy code should be temporary. Consider setting a reminder to revisit.
### Scripts vs Services
[Section titled “Scripts vs Services”](#scripts-vs-services)
pyproject.toml
```toml
[tool.unfault.rules]
exclude = [
"python.fastapi.*:scripts/*", # Scripts aren't FastAPI services
"python.missing_structured_logging:scripts/*", # CLI output, not structured logs
]
```
## Best Practices
[Section titled “Best Practices”](#best-practices)
### Do
[Section titled “Do”](#do)
* Add a reason to inline suppressions
* Use the narrowest suppression that works (inline > file > project)
* Review project-level suppressions periodically
* Use severity overrides instead of full suppression when possible
### Don’t
[Section titled “Don’t”](#dont)
* Suppress rules just to get a clean report
* Use blanket `*` suppressions without good reason
* Forget why something was suppressed (always add context)
* Suppress in production code without team discussion
## Suppression vs Configuration
[Section titled “Suppression vs Configuration”](#suppression-vs-configuration)
| Approach | Use When |
| ---------------------- | ---------------------------------------------------- |
| **Inline suppression** | Specific line has valid reason to skip the rule |
| **File suppression** | Entire file is different (script, test, generated) |
| **Project exclusion** | Rule doesn’t fit your architecture at all |
| **Severity override** | Rule applies but isn’t as important for your context |
| **Dimension filter** | You want to focus on specific areas |
## Next Steps
[Section titled “Next Steps”](#next-steps)
Configuration
Full configuration reference. [Read more](/docs/reference/configuration/)
Rules Catalog
See all available rules. [Browse rules](/docs/reference/rules/)
CI/CD Integration
Gate on specific severities. [Read more](/docs/guides/cicd/)
# Troubleshooting
> Common issues and how to resolve them.
When something doesn’t work as expected, this guide helps you figure out why.
## Analysis Issues
[Section titled “Analysis Issues”](#analysis-issues)
### ”No files found”
[Section titled “”No files found””](#no-files-found)
```plaintext
→ Analyzing project...
Found 0 matching source files
```
**Causes:**
* Wrong directory
* No supported language files
* Files excluded by ignore rules (like `.gitignore`)
**Solutions:**
Check you’re in the right place:
```bash
ls -la # Should see your source files
```
Check supported extensions exist:
```bash
find . -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.ts" | head
```
Use verbose mode for more details:
```bash
unfault review -v
```
Tip
If your source files exist but Unfault still finds `0` files, check whether they’re ignored. Unfault respects `.gitignore` (including global gitignore and `.git/info/exclude`), `.ignore`, and `.dockerignore`.
### ”Unknown language” or “Unsupported framework”
[Section titled “”Unknown language” or “Unsupported framework””](#unknown-language-or-unsupported-framework)
Unfault supports Python, Go, Rust, and TypeScript. If you’re using a different language, analysis won’t run.
For supported languages with unrecognized frameworks, analysis still works but framework-specific rules won’t apply.
### Analysis is slow
[Section titled “Analysis is slow”](#analysis-is-slow)
Large codebases take time, but if analysis seems stuck:
**Check for symlink loops:**
```bash
find . -type l -exec test -e {} \; -print
```
**Narrow the scope by running from a subdirectory:**
```bash
# Instead of the whole repo
cd src
unfault review
```
**Use verbose mode to see progress:**
```bash
unfault review -v
```
### Different results locally vs CI
[Section titled “Different results locally vs CI”](#different-results-locally-vs-ci)
Common causes:
1. **Different working directories**
Make sure you’re running from the same directory in both environments.
2. **Different git state**
```bash
# CI might have uncommitted changes excluded
git status
```
3. **Configuration differences**
Check which config is loaded with verbose mode:
```bash
unfault review -v
```
## Output Issues
[Section titled “Output Issues”](#output-issues)
### JSON output is malformed
[Section titled “JSON output is malformed”](#json-output-is-malformed)
If JSON output appears truncated or malformed:
```bash
# Ensure nothing else writes to stdout
unfault review --output json 2>/dev/null > results.json
```
### “Finding not actionable”
[Section titled ““Finding not actionable””](#finding-not-actionable)
If a finding doesn’t make sense for your code:
1. Check if it’s a false positive specific to your pattern
2. Use inline suppression with a comment explaining why:
```python
# unfault: ignore[rule.id] -- reason this doesn't apply
```
3. Report it if you think it’s a bug in the rule
## LLM Issues
[Section titled “LLM Issues”](#llm-issues)
### ”LLM provider not configured”
[Section titled “”LLM provider not configured””](#llm-provider-not-configured)
The `unfault review` LLM summary feature requires a configured LLM provider. Configure one with:
```bash
# OpenAI
unfault config llm openai --model gpt-4o
# Anthropic
unfault config llm anthropic --model claude-3-5-sonnet-latest
# Ollama (local)
unfault config llm ollama --model llama3.2
```
Check current config:
```bash
unfault config show
unfault config llm show
```
### “ANTHROPIC\_API\_KEY not set” / “OPENAI\_API\_KEY not set”
[Section titled ““ANTHROPIC\_API\_KEY not set” / “OPENAI\_API\_KEY not set””](#anthropic_api_key-not-set--openai_api_key-not-set)
The LLM provider needs an API key. Set the relevant environment variable:
```bash
export ANTHROPIC_API_KEY="..."
export OPENAI_API_KEY="sk-..."
```
Or pass the key directly when configuring:
```bash
unfault config llm anthropic --api-key "..."
```
## Observability Integration Issues
[Section titled “Observability Integration Issues”](#observability-integration-issues)
### SLO data not appearing
[Section titled “SLO data not appearing”](#slo-data-not-appearing)
If you expect SLO enrichment but it’s not showing:
1. Check integration credentials are present:
```bash
unfault config integrations show
```
2. Verify connectivity:
```bash
unfault config integrations verify
```
3. Run without `--offline` (if you were using it):
```bash
unfault review # Without --offline
```
4. If cache may be stale, refresh it:
```bash
unfault review --refresh-cache
```
### Credentials not detected
[Section titled “Credentials not detected”](#credentials-not-detected)
Unfault reads credentials from environment variables. Check the relevant provider:
| Provider | Variables |
| --------- | --------------------------------------------------------------------------- |
| GCP | Application Default Credentials via `gcloud auth application-default login` |
| Datadog | `DD_API_KEY`, `DD_APP_KEY` |
| Dynatrace | `DT_API_TOKEN`, `DT_ENVIRONMENT_URL` |
Run `unfault config integrations show` to see what Unfault detects.
## Configuration Issues
[Section titled “Configuration Issues”](#configuration-issues)
### ”Config not found” or wrong config loaded
[Section titled “”Config not found” or wrong config loaded”](#config-not-found-or-wrong-config-loaded)
Use verbose mode to see which config Unfault is loading:
```bash
unfault review -v
```
**Common issues:**
* Config is in wrong file (should be `pyproject.toml`, `Cargo.toml`, `package.json`, or `.unfault.toml`)
* TOML/JSON syntax errors in config
* Config key is misspelled
**Validate your config:**
```bash
# For TOML
python -c "import tomllib; tomllib.load(open('pyproject.toml', 'rb'))"
# For JSON
python -c "import json; json.load(open('package.json'))"
```
### Rules not being excluded
[Section titled “Rules not being excluded”](#rules-not-being-excluded)
If rules you’ve excluded still appear:
1. Check the path pattern matches:
```toml
# This excludes the rule everywhere
exclude = ["python.http.missing_timeout"]
# This excludes it only in tests/
exclude = ["python.http.missing_timeout:tests/*"]
```
2. Check for typos in rule IDs by looking at the finding output
## LSP / Editor Issues
[Section titled “LSP / Editor Issues”](#lsp--editor-issues)
### No diagnostics appearing in editor
[Section titled “No diagnostics appearing in editor”](#no-diagnostics-appearing-in-editor)
1. Check the language server output: run `unfault lsp --verbose` manually and look for errors.
2. Verify `unfault` is on your `PATH` and runs without errors.
3. Check that your project root has a recognizable manifest file (`pyproject.toml`, `Cargo.toml`, `go.mod`, `package.json`, etc.).
### Slow first analysis in editor
[Section titled “Slow first analysis in editor”](#slow-first-analysis-in-editor)
The first run builds the full semantic graph for your project. Subsequent interactions use a cache and should be fast.
## Verbose Mode
[Section titled “Verbose Mode”](#verbose-mode)
For detailed diagnostics:
```bash
unfault review -v
```
This shows:
* Which config files are loaded
* Workspace detection details
* Files found and ignored
Tip
When reporting issues, include the verbose output. It helps narrow down what’s happening.
## Getting Help
[Section titled “Getting Help”](#getting-help)
### Check your configuration
[Section titled “Check your configuration”](#check-your-configuration)
```bash
unfault config show
```
This shows your LLM and integration configuration.
### Report an issue
[Section titled “Report an issue”](#report-an-issue)
If you’ve found a bug or unexpected behavior:
1. Check existing issues: [github.com/unfault/unfault/issues](https://github.com/unfault/unfault/issues)
2. If new, open an issue with:
* Unfault version (`unfault --version`)
* OS and architecture
* Verbose output (`unfault review -v`)
* Minimal reproduction steps
## Quick Reference
[Section titled “Quick Reference”](#quick-reference)
| Symptom | Likely Cause | First Step |
| ----------------------- | --------------------------- | ---------------------------------- |
| ”No files found” | Wrong directory | Check `ls` and `pwd` |
| Slow analysis | Large codebase | Run from subdirectory |
| Different results in CI | Different working directory | Ensure same path |
| LLM not working | No provider configured | `unfault config llm show` |
| Rules not excluded | Path pattern mismatch | Check TOML syntax |
| SLO data missing | Missing credentials | `unfault config integrations show` |
# Understanding Unfamiliar Code
> Use Unfault to navigate and understand codebases you didn't write.
You’re working in a codebase you didn’t write. Maybe you just joined the team, or you’re fixing a bug in a service you’ve never touched. You need to understand how things connect before you start changing them.
Unfault’s code graph helps with this.
## Finding What Depends on a File
[Section titled “Finding What Depends on a File”](#finding-what-depends-on-a-file)
Before changing a file, see what else depends on it:
```bash
unfault graph impact src/api/auth.py
```
Output:
```plaintext
Impact analysis for src/api/auth.py:
Direct dependents (3):
→ api/routes/users.py (imports validate_token)
→ api/routes/admin.py (imports validate_token, get_permissions)
→ tests/test_auth.py (imports validate_token)
Transitive impact (7 files total)
```
This tells you the blast radius. If you change `validate_token`, those three files are directly affected. Seven files total might see different behavior.
## Finding What Depends on a Function
[Section titled “Finding What Depends on a Function”](#finding-what-depends-on-a-function)
For more precision, check a specific function:
```bash
unfault graph function-impact src/api/auth.py:validate_token
```
This shows only what depends on that function, not everything in the file.
## Finding the Most Important Files
[Section titled “Finding the Most Important Files”](#finding-the-most-important-files)
When you’re new to a codebase, start with the most connected files:
```bash
unfault graph critical
```
Output:
```plaintext
Most critical files (by dependents):
1. src/core/models.py (47 dependents)
2. src/utils/helpers.py (31 dependents)
3. src/api/auth.py (23 dependents)
4. src/db/connection.py (19 dependents)
5. src/config/settings.py (18 dependents)
```
These are the files where changes have the most impact. They’re also often the best starting point for understanding how the system fits together.
## Finding What a File Depends On
[Section titled “Finding What a File Depends On”](#finding-what-a-file-depends-on)
To see what external libraries or internal modules a file uses:
```bash
unfault graph deps src/api/client.py
```
Output:
```plaintext
Dependencies for src/api/client.py:
External:
· requests
· tenacity
· structlog
Internal:
· src/config/settings.py
· src/utils/retry.py
```
This tells you what you need to understand to work on this file.
## Finding Files That Use a Library
[Section titled “Finding Files That Use a Library”](#finding-files-that-use-a-library)
To see everywhere a library is used:
```bash
unfault graph library requests
```
Output:
```plaintext
Files using 'requests':
· src/api/client.py:12
· src/integrations/billing.py:8
· src/integrations/notifications.py:15
· scripts/health_check.py:3
```
Useful when you’re updating a dependency or changing how it’s used across the codebase.
## Querying Patterns
[Section titled “Querying Patterns”](#querying-patterns)
For understanding how something is handled across a codebase, combine review and graph commands:
```bash
# See where a library is used to understand conventions
unfault graph library requests
# Find files with stability issues to see what's been addressed elsewhere
unfault review --dimension stability --output json | jq '.contexts[].findings[] | .file_path' | sort -u
```
Run a full review first to understand the overall picture:
```bash
unfault review
```
## Before Making Changes
[Section titled “Before Making Changes”](#before-making-changes)
A practical workflow for changing unfamiliar code:
1. **Check impact first**
```bash
unfault graph impact path/to/file.py
```
2. **Understand what the file depends on**
```bash
unfault graph deps path/to/file.py
```
3. **Find how similar things are done**
```bash
unfault graph library
```
4. **Make your changes**
5. **Review before committing**
```bash
unfault review
```
## With AI Agents
[Section titled “With AI Agents”](#with-ai-agents)
If you’re using an AI assistant to modify unfamiliar code, add to your AGENTS.md:
```text
Before modifying code in unfamiliar areas:
1. Run: unfault graph impact
2. If impact is large (>10 files), mention this before proceeding
3. Run: unfault graph deps to understand what it relies on
4. After changes, run: unfault review
```
This gives the agent structural context it wouldn’t otherwise have.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Code Graph Reference
All graph commands and options. [Read more](/docs/guides/code-graph/)
Pre-commit Review
Review your changes before committing. [Read more](/docs/guides/pre-commit-review/)
Querying the Codebase
Graph and review commands for exploration. [Read more](/docs/guides/asking-questions/)
Use with AI Agents
Integrate with AI coding assistants. [Read more](/docs/guides/agents/)
# LSP / Editor Integration
> Using the Unfault language server with your editor.
Caution
A packaged VS Code extension is not currently distributed. This page describes the LSP server that ships with the CLI. It can be connected to editors that support LSP, but requires manual configuration.
## The LSP Server
[Section titled “The LSP Server”](#the-lsp-server)
The Unfault CLI includes a full Language Server Protocol server. Start it with:
```bash
unfault lsp
```
It communicates over stdio, which is the standard transport for most LSP clients.
The LSP server runs the same local analysis pipeline as `unfault review` (Tree-sitter parsing, semantic graph construction, and rule analysis) and publishes diagnostics in real time as you work.
## What the LSP Provides
[Section titled “What the LSP Provides”](#what-the-lsp-provides)
* **Diagnostics**: Inline squiggles for findings (Error for critical/high, Warning for medium, Info for low)
* **Code actions**: Quick-fix suggestions from structured patches or unified diffs
* **File centrality**: Custom notification `unfault/fileCentrality` (how central the current file is in the dependency graph)
* **File dependencies**: Custom notification `unfault/fileDependencies` (files that depend on the current one)
* **Hover**: Function impact analysis using call graph traversal
## Connecting an Editor
[Section titled “Connecting an Editor”](#connecting-an-editor)
### Neovim (nvim-lspconfig)
[Section titled “Neovim (nvim-lspconfig)”](#neovim-nvim-lspconfig)
```lua
local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')
if not configs.unfault then
configs.unfault = {
default_config = {
cmd = { 'unfault', 'lsp' },
filetypes = { 'python', 'go', 'rust', 'typescript', 'javascript' },
root_dir = lspconfig.util.root_pattern(
'pyproject.toml', 'Cargo.toml', 'go.mod', 'package.json'
),
},
}
end
lspconfig.unfault.setup {}
```
### VS Code (manual)
[Section titled “VS Code (manual)”](#vs-code-manual)
Create `.vscode/settings.json` with a custom LSP client extension, or use a generic LSP client extension that accepts a command. Point it at `unfault lsp --stdio`.
## Supported Languages
[Section titled “Supported Languages”](#supported-languages)
* Python
* Go
* Rust
* TypeScript / JavaScript
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
**No diagnostics appearing**
1. Check the language server output: run `unfault lsp --verbose` manually and look for errors.
2. Verify `unfault` is on your `PATH` and runs without errors.
3. Check that your project root has a recognizable manifest file (`pyproject.toml`, `Cargo.toml`, `go.mod`, `package.json`, etc.).
**Slow first analysis**
The first run builds the full semantic graph for your project. Subsequent interactions use a cache.
# Installation
> How to install the Unfault CLI.
Unfault is an open-source CLI. No account required. Download the binary and run it against your code.
## Install the CLI
[Section titled “Install the CLI”](#install-the-cli)
* macOS (Apple Silicon)
```bash
mkdir -p ~/.local/bin
curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-aarch64-apple-darwin
chmod +x ~/.local/bin/unfault
```
* macOS (Intel)
```bash
mkdir -p ~/.local/bin
curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-x86_64-apple-darwin
chmod +x ~/.local/bin/unfault
```
* Linux (x86\_64)
```bash
mkdir -p ~/.local/bin
curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-x86_64-unknown-linux-gnu
chmod +x ~/.local/bin/unfault
```
* Linux (aarch64)
```bash
mkdir -p ~/.local/bin
curl -L -o ~/.local/bin/unfault https://github.com/unfault/cli/releases/latest/download/unfault-aarch64-unknown-linux-gnu
chmod +x ~/.local/bin/unfault
```
* Windows (PowerShell)
```powershell
$dest = "$env:USERPROFILE\bin\unfault.exe"
New-Item -ItemType Directory -Force (Split-Path $dest) | Out-Null
Invoke-WebRequest -Uri "https://github.com/unfault/cli/releases/latest/download/unfault-x86_64-pc-windows-msvc.exe" -OutFile $dest
```
* Cargo (from source)
```bash
cargo install unfault
```
Downloads go to the [latest release on GitHub](https://github.com/unfault/cli/releases/latest). Put the binary somewhere on your `PATH` (e.g. `~/.local/bin` on Linux/macOS).
Verify it works:
```bash
unfault --version
```
## Optional: Configure an LLM
[Section titled “Optional: Configure an LLM”](#optional-configure-an-llm)
Some features (like AI-powered review summaries) require a configured LLM provider. This is optional. The core `review` and `graph` commands work without it.
Supported providers: OpenAI, Anthropic, Ollama, any OpenAI-compatible endpoint.
```bash
# OpenAI
unfault config llm openai --model gpt-4o
# Anthropic
unfault config llm anthropic --model claude-3-5-sonnet-latest
# Local Ollama
unfault config llm ollama --model llama3.2
```
See [Configuration](/docs/reference/configuration/) for the full reference.
## Optional: Configure Observability Integrations
[Section titled “Optional: Configure Observability Integrations”](#optional-configure-observability-integrations)
To enrich reviews with SLO data from your cloud platform, set the relevant credentials before running `unfault review`:
| Provider | Required Variables |
| -------------------- | ------------------------------------------------------------------------- |
| GCP Cloud Monitoring | Application Default Credentials (`gcloud auth application-default login`) |
| Datadog | `DD_API_KEY`, `DD_APP_KEY` |
| Dynatrace | `DT_API_TOKEN`, `DT_ENVIRONMENT_URL` |
Run `unfault config integrations show` to check what’s detected, or `unfault config integrations verify` to test connectivity.
## Next Steps
[Section titled “Next Steps”](#next-steps)
Quick Start
Run your first review and understand the output. [Get started](/docs/quick-start)
How It Works
Understand what happens when you run a review. [Learn more](/docs/concepts/how-it-works)
# Quick Start
> Run your first Unfault review in under a minute.
This guide walks you through running your first review and understanding the results.
## Run Your First Review
[Section titled “Run Your First Review”](#run-your-first-review)
Navigate to your project directory and run:
```bash
unfault review
```
Unfault parses your code locally and shows what it finds:
```plaintext
Worth looking out for
🟡 main.py:12 · The Slow Death
FastAPI app `app` has no request timeout middleware
↳ puts App A Availability SLO at risk (100%)
A downstream dependency slows down and your service holds
threads/connections until it saturates and dies.
Tradeoff
↳ Simplicity no timeout means less code and fewer
configuration decisions at call time.
↳ Systemic Availability a single slow dependency can exhaust the thread
pool and take down the entire service.
🟡 main.py:36 · The Retry Storm
HTTP call via `httpx`.AsyncClient has no retry policy
↳ puts App A Availability SLO at risk (100%)
During an outage your service retries failures instantly, preventing the
downstream service from ever recovering.
Tradeoff
↳ Local Availability retries transparently mask transient failures
from the caller, improving perceived reliability.
↳ Systemic Metastability synchronized retries with no backoff create
thunderstorms that prevent downstream services
from ever recovering.
app-a python · fastapi · 1 file [ parse 5ms engine 0ms fetch 7849ms ]
```
Each finding names the failure mode, links it to any discovered SLOs at risk, explains the tradeoff, and gives a file and line number.
## Drilling Into Details
[Section titled “Drilling Into Details”](#drilling-into-details)
To see full findings with suggested fixes:
```bash
unfault review --output full
```
This shows each finding with its location, explanation, and a suggested code change.
## Output Formats
[Section titled “Output Formats”](#output-formats)
Choose the format that fits your workflow:
* Default (basic)
```bash
# Header + summary (good for the terminal)
unfault review
```
* Concise
```bash
# Statistics only
unfault review --output concise
```
* Full
```bash
# Full findings with diffs
unfault review --output full
```
* JSON
```bash
# Machine-readable for CI/CD or scripting
unfault review --output json
```
* SARIF
```bash
# GitHub Code Scanning
unfault review --output sarif
```
## Filtering by Dimension
[Section titled “Filtering by Dimension”](#filtering-by-dimension)
Focus on specific types of issues:
```bash
# Only stability issues
unfault review --dimension stability
# Only performance issues
unfault review --dimension performance
# Multiple dimensions
unfault review --dimension stability --dimension correctness
```
Available dimensions: `stability`, `correctness`, `performance`, `scalability`.
## CI/CD Integration
[Section titled “CI/CD Integration”](#cicd-integration)
Use exit codes to gate deployments:
```bash
unfault review
if [ $? -eq 5 ]; then
echo "Findings detected. Blocking deployment."
exit 1
fi
```
Tip
Exit code `5` means findings were detected. See the [CLI reference](/docs/reference/cli/#exit-codes) for all exit codes.
## Explore the Code Graph
[Section titled “Explore the Code Graph”](#explore-the-code-graph)
Before a refactor, check what depends on a file:
```bash
unfault graph impact src/api/auth.py
```
Find the most heavily-connected files:
```bash
unfault graph critical
```
See all files using a library:
```bash
unfault graph library requests
```
## Next Steps
[Section titled “Next Steps”](#next-steps)
CLI Commands
Learn all CLI commands in detail. [Read more](/docs/guides/cli/)
Code Graph
Explore dependencies and blast radius. [Read more](/docs/guides/code-graph/)
CI/CD Pipeline
Add Unfault to your build process. [Read more](/docs/guides/cicd/)
AI Agents
Give your coding assistant codebase context. [Read more](/docs/guides/agents/)
# Output Reference
> JSON schemas for unfault review output.
This page documents the JSON output schema for `unfault review --output json`. Useful when parsing CLI output programmatically or building integrations.
Tip
For background on core concepts, see [Dimensions](/docs/concepts/dimensions/).
## Review JSON Output
[Section titled “Review JSON Output”](#review-json-output)
When you run `unfault review --output json`, the CLI prints a JSON object with the analysis results.
### Top-Level Schema
[Section titled “Top-Level Schema”](#top-level-schema)
```json
{
"meta": { ... },
"contexts": [ ... ],
"elapsed_ms": 142
}
```
### Meta Object
[Section titled “Meta Object”](#meta-object)
```json
{
"label": "payments-service",
"languages": ["python"],
"framework_guesses": ["fastapi"],
"requested_dimensions": ["stability", "correctness"]
}
```
| Field | Type | Description |
| ---------------------- | ------ | ------------------------------------------------ |
| `label` | string | Workspace label (usually directory name) |
| `languages` | array | Detected programming languages |
| `framework_guesses` | array | Detected frameworks (e.g., `fastapi`, `express`) |
| `requested_dimensions` | array | Dimensions that were analyzed |
### Context Object
[Section titled “Context Object”](#context-object)
Each context groups findings together. Currently there is one context per workspace:
```json
{
"context_id": "ctx_workspace",
"label": "Workspace",
"findings": [ ... ]
}
```
### Finding Object
[Section titled “Finding Object”](#finding-object)
A finding is a pattern detected by a rule:
```json
{
"id": "python.http.missing_timeout:api/client.py:42",
"rule_id": "python.http.missing_timeout",
"kind": "StabilityRisk",
"title": "HTTP call has no timeout",
"description": "This HTTP call via requests.get has no timeout specified...",
"severity": "High",
"confidence": 0.9,
"dimension": "stability",
"file_path": "api/client.py",
"line": 42,
"column": 5,
"end_line": 42,
"end_column": 45,
"fix_preview": "Add timeout parameter: requests.get(url, timeout=30)"
}
```
| Field | Type | Description |
| ------------- | --------------- | ---------------------------------------------------- |
| `id` | string | Unique finding identifier (`rule_id:file_path:line`) |
| `rule_id` | string | Rule that produced this finding |
| `kind` | enum | Finding category (see below) |
| `title` | string | Short title |
| `description` | string | Detailed explanation |
| `severity` | enum | `Critical`, `High`, `Medium`, `Low`, `Info` |
| `confidence` | float | Confidence score (0.0 to 1.0) |
| `dimension` | string | Which dimension this finding relates to |
| `file_path` | string | File path where the finding was detected |
| `line` | integer | Line number (1-based) |
| `column` | integer \| null | Column number (1-based) |
| `end_line` | integer \| null | End line number |
| `end_column` | integer \| null | End column number |
| `fix_preview` | string \| null | Human-readable summary of suggested fix |
### Finding Kinds
[Section titled “Finding Kinds”](#finding-kinds)
| Kind | Description |
| ----------------------- | --------------------------------------------- |
| `StabilityRisk` | Could cause service instability or outages |
| `PerformanceSmell` | May impact performance |
| `BehaviorThreat` | Unexpected behavior under certain conditions |
| `AntiPattern` | Code pattern that typically leads to problems |
| `ResourceLeak` | Potential resource leak (connections, memory) |
| `ReliabilityRisk` | Could affect service reliability |
| `SecurityVulnerability` | Security concern |
### Severity Levels
[Section titled “Severity Levels”](#severity-levels)
| Severity | Description |
| ---------- | ------------------------- |
| `Critical` | Immediate action required |
| `High` | Should be addressed soon |
| `Medium` | Worth addressing |
| `Low` | Minor improvement |
| `Info` | Informational |
### Dimensions
[Section titled “Dimensions”](#dimensions)
| Dimension | Description |
| ----------------- | ---------------------------------- |
| `stability` | Service stability and resilience |
| `performance` | Execution speed and resource usage |
| `correctness` | Logic and behavior correctness |
| `scalability` | Ability to handle increased load |
| `observability` | Logging, tracing, and monitoring |
| `reliability` | Consistent operation over time |
| `security` | Security posture |
| `maintainability` | Code maintainability |
### Complete Example
[Section titled “Complete Example”](#complete-example)
```json
{
"meta": {
"label": "payments-service",
"languages": ["python"],
"framework_guesses": ["fastapi"],
"requested_dimensions": ["stability", "correctness"]
},
"contexts": [
{
"context_id": "ctx_workspace",
"label": "Workspace",
"findings": [
{
"id": "python.http.missing_timeout:src/client.py:42",
"rule_id": "python.http.missing_timeout",
"kind": "StabilityRisk",
"title": "HTTP call has no timeout",
"description": "This HTTP call via requests.get has no timeout specified. If the remote service is slow or unresponsive, this call will block indefinitely.",
"severity": "High",
"confidence": 0.9,
"dimension": "stability",
"file_path": "src/client.py",
"line": 42,
"column": 5,
"fix_preview": "Add timeout parameter: requests.get(url, timeout=30)"
}
]
}
],
"elapsed_ms": 142
}
```
***
## Exit Codes
[Section titled “Exit Codes”](#exit-codes)
| Code | Meaning |
| ---- | -------------------- |
| 0 | Success, no findings |
| 1 | General error |
| 2 | Configuration error |
| 4 | Network error |
| 5 | Findings detected |
| 6 | Invalid input |
Tip
Exit code 5 indicates findings were detected. This is not an error. Use it in CI to gate deployments.
# CLI Commands
> Complete reference for the Unfault command-line interface.
Complete reference for all Unfault CLI commands.
## Commands
[Section titled “Commands”](#commands)
| Command | Purpose |
| ------------------- | ---------------------------------------------- |
| [`review`](#review) | Analyze code for production-readiness |
| [`lint`](#lint) | Show all findings grouped by severity and rule |
| [`graph`](#graph) | Explore code dependencies and impact |
| [`info`](#info) | Look up SRE glossary entries |
| [`config`](#config) | Manage CLI settings |
| [`lsp`](#lsp) | Start the language server for IDE integration |
***
## `review`
[Section titled “review”](#review)
Analyze code and get recommendations. Runs entirely locally.
```bash
unfault review [OPTIONS]
```
### Options
[Section titled “Options”](#options)
| Option | Description | Default |
| ----------------------- | ----------------------------------------------------------------------------------------------------- | ---------------- |
| `--output ` | Output format: `basic`, `concise`, `full`, `json`, `sarif` | `basic` |
| `-v, --verbose` | Enable verbose output | Disabled |
| `--profile ` | Override the detected profile (e.g., `python_fastapi_backend`) | Auto-detected |
| `-d, --dimension ` | Dimensions to analyze (repeatable). Options: `stability`, `correctness`, `performance`, `scalability` | All from profile |
| `--fix` | Auto-apply all suggested fixes | Disabled |
| `--dry-run` | Show fixes without applying them | Disabled |
| `--all` | Show all findings in full (equivalent to `unfault lint`) | Disabled |
| `--refresh-cache` | Discard enrichment cache and re-fetch SLOs and traces | Disabled |
| `--offline` | Skip SLO and trace fetching entirely (useful in CI) | Disabled |
### File Discovery and Ignores
[Section titled “File Discovery and Ignores”](#file-discovery-and-ignores)
Unfault respects common ignore conventions:
* `.gitignore` (including global gitignore and `.git/info/exclude`)
* `.ignore`
* `.dockerignore`
It also skips common dependency/build directories (`node_modules`, `target`, `dist`, `build`, `.venv`) even if they are not explicitly ignored.
### Output Formats
[Section titled “Output Formats”](#output-formats)
| Format | Use Case |
| --------- | ---------------------------------------------------------- |
| `basic` | Default. Header, summary, and guidance on next steps. |
| `concise` | Brief statistics only. Good for dashboards. |
| `full` | Detailed findings with file locations and suggested fixes. |
| `json` | Machine-readable. Use for CI/CD parsing or custom tooling. |
| `sarif` | GitHub Code Scanning and IDE integration. |
### Examples
[Section titled “Examples”](#examples)
```bash
# Standard review
unfault review
# Focus on stability issues only
unfault review --dimension stability
# Full details with suggested fixes
unfault review --output full
# JSON for CI parsing
unfault review --output json
# Skip observability enrichment
unfault review --offline
# Preview fixes without applying
unfault review --dry-run
```
***
## `lint`
[Section titled “lint”](#lint)
Show all findings grouped by severity and rule, the detailed linter view.
```bash
unfault lint [OPTIONS]
```
### Options
[Section titled “Options”](#options-1)
| Option | Description | Default |
| ----------------------- | ---------------------------------- | ---------------- |
| `--output ` | Output format: `basic`, `json` | `basic` |
| `-v, --verbose` | Enable verbose output | Disabled |
| `--profile ` | Override detected profile | Auto-detected |
| `-d, --dimension ` | Dimensions to analyze (repeatable) | All from profile |
| `--fix` | Auto-apply all suggested fixes | Disabled |
| `--dry-run` | Show fixes without applying them | Disabled |
***
## `graph`
[Section titled “graph”](#graph)
Query the code graph for dependencies, impact analysis, and critical files. Runs locally.
```bash
unfault graph
```
### Subcommands
[Section titled “Subcommands”](#subcommands)
| Subcommand | Purpose |
| ------------------------------------------- | -------------------------------------- |
| [`impact`](#graph-impact) | What breaks if I change this file? |
| [`function-impact`](#graph-function-impact) | What breaks if I change this function? |
| [`library`](#graph-library) | Which files use a specific library? |
| [`deps`](#graph-deps) | What does this file depend on? |
| [`critical`](#graph-critical) | Which files are most critical? |
| [`stats`](#graph-stats) | Code graph statistics |
| [`dump`](#graph-dump) | Dump graph for debugging |
***
### `graph impact`
[Section titled “graph impact”](#graph-impact)
Analyze what depends on a file. Useful before refactoring.
```bash
unfault graph impact [OPTIONS]
```
| Option | Description | Default |
| ------------------------ | -------------------------------- | ----------------- |
| `` | File path to analyze | Required |
| `-w, --workspace ` | Workspace path | Current directory |
| `--max-depth ` | Transitive analysis depth (1-10) | 5 |
| `--json` | Output as JSON | Disabled |
| `-v, --verbose` | Verbose output | Disabled |
```bash
unfault graph impact src/api/auth.py
unfault graph impact --max-depth 3 src/core/models.py
```
***
### `graph function-impact`
[Section titled “graph function-impact”](#graph-function-impact)
Analyze what depends on a specific function.
```bash
unfault graph function-impact [OPTIONS]
```
| Option | Description | Default |
| ------------------------ | ---------------------------------- | ----------------- |
| `` | Function in format `file:function` | Required |
| `-w, --workspace ` | Workspace path | Current directory |
| `--max-depth ` | Transitive analysis depth (1-10) | 5 |
| `--json` | Output as JSON | Disabled |
```bash
unfault graph function-impact src/api/auth.py:validate_token
```
***
### `graph library`
[Section titled “graph library”](#graph-library)
Find files that use a specific library.
```bash
unfault graph library [OPTIONS]
```
| Option | Description | Default |
| ------------------------ | ------------------------------------------ | ----------------- |
| `` | Library name (e.g., `requests`, `fastapi`) | Required |
| `-w, --workspace ` | Workspace path | Current directory |
| `--json` | Output as JSON | Disabled |
```bash
unfault graph library requests
unfault graph library sqlalchemy --json
```
***
### `graph deps`
[Section titled “graph deps”](#graph-deps)
Find external dependencies of a file.
```bash
unfault graph deps [OPTIONS]
```
| Option | Description | Default |
| ------------------------ | -------------------- | ----------------- |
| `` | File path to analyze | Required |
| `-w, --workspace ` | Workspace path | Current directory |
| `--json` | Output as JSON | Disabled |
```bash
unfault graph deps src/api/routes.py
```
***
### `graph critical`
[Section titled “graph critical”](#graph-critical)
Find the most critical files in the codebase (high connectivity, many dependents).
```bash
unfault graph critical [OPTIONS]
```
| Option | Description | Default |
| ------------------------ | -------------------------------- | ----------------- |
| `-n, --limit ` | Number of files to return (1-50) | 10 |
| `--sort-by ` | Sort metric (see below) | `in-degree` |
| `-w, --workspace ` | Workspace path | Current directory |
| `--json` | Output as JSON | Disabled |
**Sort metrics:**
| Metric | Meaning |
| ------------------ | ----------------------------- |
| `in-degree` | Files most imported by others |
| `out-degree` | Files that import the most |
| `total-degree` | Total connectivity |
| `library-usage` | External libraries used |
| `importance-score` | Weighted importance |
```bash
unfault graph critical
unfault graph critical --limit 20 --sort-by total-degree
```
***
### `graph stats`
[Section titled “graph stats”](#graph-stats)
Get code graph statistics.
```bash
unfault graph stats [OPTIONS]
```
| Option | Description | Default |
| ------------------------ | -------------- | ----------------- |
| `-w, --workspace ` | Workspace path | Current directory |
| `--json` | Output as JSON | Disabled |
***
### `graph dump`
[Section titled “graph dump”](#graph-dump)
Dump the local code graph for debugging.
```bash
unfault graph dump [OPTIONS]
```
| Option | Description | Default |
| ------------------------ | ----------------------- | ----------------- |
| `-w, --workspace ` | Workspace path | Current directory |
| `--calls-only` | Output only call edges | Disabled |
| `--file ` | Dump only specific file | All files |
***
## `info`
[Section titled “info”](#info)
Look up SRE glossary entries for failure modes.
```bash
unfault info
```
Available IDs: `SLO-001` through `SLO-006`.
```bash
unfault info SLO-001 # Slow Death
unfault info SLO-002 # Retry Storm
unfault info SLO-003 # Zombie Process
unfault info SLO-004 # Thundering Herd
unfault info SLO-005 # Blackhole
unfault info SLO-006 # Cascade
```
***
## `config`
[Section titled “config”](#config)
Manage CLI configuration.
```bash
unfault config
```
### Subcommands
[Section titled “Subcommands”](#subcommands-1)
| Subcommand | Purpose |
| -------------- | ------------------------------------------------------ |
| `show` | Display current configuration |
| `llm` | Manage LLM provider configuration |
| `integrations` | Inspect and verify observability integrations |
| `agent` | Generate agent skill files for Claude Code or OpenCode |
***
### `config show`
[Section titled “config show”](#config-show)
Display current configuration (secrets masked by default).
```bash
unfault config show [--show-secrets]
```
***
### `config llm`
[Section titled “config llm”](#config-llm)
Configure an LLM provider for AI-powered review summaries.
```bash
unfault config llm
```
#### OpenAI
[Section titled “OpenAI”](#openai)
```bash
unfault config llm openai [OPTIONS]
```
| Option | Description | Default |
| --------------------- | ------------------------------------------ | -------- |
| `-m, --model ` | Model name | `gpt-4` |
| `-k, --api-key ` | API key (prefers `OPENAI_API_KEY` env var) | From env |
```bash
unfault config llm openai --model gpt-4o
```
#### Anthropic
[Section titled “Anthropic”](#anthropic)
```bash
unfault config llm anthropic [OPTIONS]
```
| Option | Description | Default |
| --------------------- | --------------------------------------------- | -------------------------- |
| `-m, --model ` | Model name | `claude-3-5-sonnet-latest` |
| `-k, --api-key ` | API key (prefers `ANTHROPIC_API_KEY` env var) | From env |
```bash
unfault config llm anthropic --model claude-sonnet-4-5
```
#### Ollama
[Section titled “Ollama”](#ollama)
```bash
unfault config llm ollama [OPTIONS]
```
| Option | Description | Default |
| ---------------------- | ------------------- | ------------------------ |
| `-e, --endpoint ` | Ollama API endpoint | `http://localhost:11434` |
| `-m, --model ` | Model name | `llama3.2` |
```bash
unfault config llm ollama --model mistral
```
#### Custom (OpenAI-compatible)
[Section titled “Custom (OpenAI-compatible)”](#custom-openai-compatible)
```bash
unfault config llm custom --endpoint --model [OPTIONS]
```
| Option | Description | Default |
| ---------------------- | ------------ | -------- |
| `-e, --endpoint ` | API endpoint | Required |
| `-m, --model ` | Model name | Required |
| `-k, --api-key ` | API key | None |
#### Other LLM commands
[Section titled “Other LLM commands”](#other-llm-commands)
```bash
unfault config llm show # Show current LLM config
unfault config llm show --show-secrets
unfault config llm remove # Remove LLM config
```
***
### `config integrations`
[Section titled “config integrations”](#config-integrations)
Inspect and verify observability provider credentials.
```bash
unfault config integrations show # Show detected integrations (no network calls)
unfault config integrations verify # Verify by making live API calls
```
Supported providers: GCP Cloud Monitoring, Datadog, Dynatrace.
***
### `config agent`
[Section titled “config agent”](#config-agent)
Generate agent skill files so Claude Code or OpenCode can use Unfault as a structured tool.
```bash
unfault config agent claude [--global] [--dry-run]
unfault config agent opencode [--global] [--dry-run]
```
| Option | Description |
| ----------- | --------------------------------------------------------------------------------------------- |
| `--global` | Write to `~/.claude/skills/` or `~/.config/opencode/skills/` instead of the project directory |
| `--dry-run` | Print what would be created without writing files |
***
## `lsp`
[Section titled “lsp”](#lsp)
Start the Language Server Protocol server for IDE integration (stdio transport).
```bash
unfault lsp [OPTIONS]
```
| Option | Description | Default |
| --------------- | -------------------------------- | -------- |
| `-v, --verbose` | Enable verbose logging to stderr | Disabled |
Note
The LSP server is intended to be started by a compatible editor, not invoked directly. It communicates over stdio.
***
## Exit Codes
[Section titled “Exit Codes”](#exit-codes)
| Code | Meaning | Action |
| ---- | -------------------- | ------------------- |
| 0 | Success, no findings | Proceed |
| 1 | General error | Check error message |
| 2 | Configuration error | Check config |
| 4 | Network error | Check connectivity |
| 5 | Findings detected | Review findings |
| 6 | Invalid input | Check arguments |
Tip
Exit code 5 indicates findings were detected. This is not an error. Use it in CI to gate deployments.
***
## Environment Variables
[Section titled “Environment Variables”](#environment-variables)
| Variable | Description |
| ------------------------------------- | ---------------------------------------- |
| `OPENAI_API_KEY` | OpenAI API key for LLM features |
| `ANTHROPIC_API_KEY` | Anthropic API key for LLM features |
| `DD_API_KEY` / `DD_APP_KEY` | Datadog credentials for SLO enrichment |
| `DT_API_TOKEN` / `DT_ENVIRONMENT_URL` | Dynatrace credentials for SLO enrichment |
***
## Configuration File
[Section titled “Configuration File”](#configuration-file)
The CLI stores configuration at:
| Platform | Location |
| ------------- | ------------------------------------------- |
| Linux / macOS | `~/.config/unfault/config.json` |
| Windows | `%USERPROFILE%\.config\unfault\config.json` |
# Configuration
> Configuration options for Unfault CLI and workspaces.
Unfault can be configured at the user level and per-workspace.
## User Configuration
[Section titled “User Configuration”](#user-configuration)
The configuration file location depends on your operating system:
| Platform | Location |
| ----------- | ------------------------------------------------------------------------- |
| **Linux** | `~/.config/unfault/config.json` or `$XDG_CONFIG_HOME/unfault/config.json` |
| **macOS** | `~/.config/unfault/config.json` or `$XDG_CONFIG_HOME/unfault/config.json` |
| **Windows** | `%USERPROFILE%\.config\unfault\config.json` |
Note
All platforms use the same `.config` directory pattern for consistency, rather than platform-specific locations like `~/Library/Application Support` (macOS) or `%APPDATA%` (Windows).
Example configuration:
```json
{
"llm": {
"provider": "openai",
"model": "gpt-4o",
"api_key": "sk-..."
}
}
```
### LLM Configuration
[Section titled “LLM Configuration”](#llm-configuration)
To enable AI-powered review summaries, configure an LLM provider:
* OpenAI
```bash
unfault config llm openai --model gpt-5.1
```
* Anthropic
```bash
unfault config llm anthropic --model claude-4-5-sonnet-latest
```
* Ollama
```bash
unfault config llm ollama --model llama3.2
```
## Workspace Configuration
[Section titled “Workspace Configuration”](#workspace-configuration)
Unfault reads configuration from your project’s manifest file, avoiding the need for a dedicated config file. The configuration location depends on your language:
* Python (pyproject.toml)
```toml
[tool.unfault]
# Override auto-detected profile
profile = "python_fastapi_backend"
# Limit analysis to specific dimensions
dimensions = ["stability", "correctness", "performance"]
[tool.unfault.rules]
# Rules to exclude (supports glob patterns)
exclude = [
"python.missing_structured_logging", # We use custom logging
"python.http.*", # All HTTP rules
]
# Additional rules to include
include = ["python.security.*"]
# Severity overrides
[tool.unfault.rules.severity]
"python.bare_except" = "low"
```
* Rust (Cargo.toml)
```toml
[package.metadata.unfault]
profile = "rust_axum_service"
dimensions = ["stability", "correctness"]
[package.metadata.unfault.rules]
exclude = ["rust.println_in_lib"]
[package.metadata.unfault.rules.severity]
"rust.unsafe_unwrap" = "critical"
```
* JavaScript/TypeScript (package.json)
```json
{
"name": "my-app",
"unfault": {
"profile": "typescript_express_backend",
"dimensions": ["stability", "security"],
"rules": {
"exclude": ["typescript.console_in_production"],
"include": ["typescript.security.*"],
"severity": {
"typescript.empty_catch": "critical"
}
}
}
}
```
* Standalone (.unfault.toml)
```toml
# For Go projects, multi-language repos, or any project
# where you prefer a dedicated config file
# Override auto-detected profile (optional)
profile = "go_gin_service"
# Limit analysis to specific dimensions (optional)
# Available: stability, correctness, performance, scalability, security, maintainability
dimensions = ["stability", "performance"]
[rules]
# Rules to exclude - supports exact IDs and glob patterns
exclude = [
"go.missing_structured_logging",
"go.http.*", # All HTTP-related rules
]
# Additional rules to include beyond profile defaults
include = ["go.security.*"]
# Severity overrides: low, medium, high, critical
[rules.severity]
"go.unchecked_error" = "critical"
"go.defer_in_loop" = "low"
```
Tip
Use `.unfault.toml` when your project doesn’t have a `pyproject.toml`, `Cargo.toml`, or `package.json`, or when you prefer to keep Unfault configuration separate from your package manifest.
### Configuration Priority
[Section titled “Configuration Priority”](#configuration-priority)
When multiple configuration sources exist:
1. **Manifest files are checked first** - `pyproject.toml`, `Cargo.toml`, then `package.json`
2. **`.unfault.toml` is the fallback** - Used when no manifest contains unfault configuration
3. **No merging** - Only one source is used per project (the first one found with unfault config)
### Rule Patterns
[Section titled “Rule Patterns”](#rule-patterns)
Patterns for `exclude` and `include` use glob syntax:
| Pattern | Matches | Does Not Match |
| ----------------------------- | ---------------------------------------------------------- | ----------------------------- |
| `python.http.missing_timeout` | Exact match only | Any other rule |
| `python.http.*` | `python.http.missing_timeout`, `python.http.missing_retry` | `python.http.client.timeout` |
| `*.missing_timeout` | `python.missing_timeout`, `go.missing_timeout` | `python.http.missing_timeout` |
| `python.**` | All rules starting with `python.` | Rules from other languages |
### Disabling Rules
[Section titled “Disabling Rules”](#disabling-rules)
To disable a rule project-wide, add it to the `exclude` list in your config.
You can also disable rules inline in your code:
* Go
```go
// unfault:disable http_client_missing_timeout
client := http.Client{}
```
* Python
```python
# unfault:disable-next-line n_plus_one_query
for user in users:
orders = get_orders(user.id)
```
## Environment Variables
[Section titled “Environment Variables”](#environment-variables)
### LLM Providers
[Section titled “LLM Providers”](#llm-providers)
Used by the AI-powered review summary feature:
| Variable | Description |
| ------------------- | ---------------------------------------------------------------------- |
| `OPENAI_API_KEY` | OpenAI API key. Required when using `unfault config llm openai`. |
| `ANTHROPIC_API_KEY` | Anthropic API key. Required when using `unfault config llm anthropic`. |
### SLO Discovery (GCP)
[Section titled “SLO Discovery (GCP)”](#slo-discovery-gcp)
Used when running `unfault review` with GCP credentials configured:
| Variable | Description |
| -------------------------------- | ------------------------------------------------------------- |
| `GOOGLE_APPLICATION_CREDENTIALS` | Path to service account JSON file. Takes precedence over ADC. |
| `GOOGLE_CLOUD_PROJECT` | GCP project ID. Overrides auto-detection from credentials. |
| `GCP_PROJECT` | Alternative to `GOOGLE_CLOUD_PROJECT`. |
| `GCLOUD_PROJECT` | Alternative to `GOOGLE_CLOUD_PROJECT`. |
Tip
For local development, run `gcloud auth application-default login` instead of setting credentials manually. Unfault reads Application Default Credentials from `~/.config/gcloud/application_default_credentials.json`.
### SLO Discovery (Datadog)
[Section titled “SLO Discovery (Datadog)”](#slo-discovery-datadog)
Used when Datadog credentials are present:
| Variable | Description |
| ------------ | ------------------------------------------------------------------------ |
| `DD_API_KEY` | Datadog API key. Required for Datadog integration. |
| `DD_APP_KEY` | Datadog application key. Required for Datadog integration. |
| `DD_SITE` | Datadog site (e.g., `datadoghq.eu` for EU). Defaults to `datadoghq.com`. |
### SLO Discovery (Dynatrace)
[Section titled “SLO Discovery (Dynatrace)”](#slo-discovery-dynatrace)
Used when Dynatrace credentials are present:
| Variable | Description |
| -------------------- | ------------------------------------------------------------------------ |
| `DT_API_TOKEN` | Dynatrace API token with SLO read permissions. |
| `DT_ENVIRONMENT_URL` | Dynatrace environment URL (e.g., `https://abc12345.live.dynatrace.com`). |
### Configuration Paths
[Section titled “Configuration Paths”](#configuration-paths)
| Variable | Description |
| ----------------- | ----------------------------------------------------------------------- |
| `XDG_CONFIG_HOME` | Override config directory. Defaults to `~/.config`. |
| `HOME` | User home directory (Linux/macOS). Used to locate `~/.config/unfault/`. |
| `USERPROFILE` | User home directory (Windows). Used to locate `.config\unfault\`. |
## Configuration Precedence
[Section titled “Configuration Precedence”](#configuration-precedence)
Settings are applied in this order (later overrides earlier):
1. Default values
2. User config (see [User Configuration](#user-configuration) for platform-specific paths)
3. Workspace config (`pyproject.toml`, `Cargo.toml`, `package.json`, or `.unfault.toml`)
4. Environment variables
5. Command-line flags
# RAG Query Reference
> Reference for the Unfault RAG query system.
Caution
This page describes functionality that is not yet available in the current open-source CLI release.
RAG-based query capabilities (natural language questions over your codebase) are planned for a future release. This page will be updated when the feature ships.
In the meantime, for exploring your codebase:
* Use `unfault graph impact ` to understand blast radius
* Use `unfault graph critical` to find hub files
* Use `unfault graph library ` to find where a dependency is used
* Use `unfault graph deps ` to see what a file depends on
See the [CLI Reference](/docs/reference/cli/#graph) for the full graph command reference.
# Rules Reference
> Complete reference for all Unfault detection rules organized by language.
Unfault analyzes your code across **195 production-readiness rules** in Python, Go, Rust, and TypeScript. Each rule targets patterns that cause real incidents in production systems.
## Rules by Language
[Section titled “Rules by Language”](#rules-by-language)
[Python Rules ](/docs/reference/rules/python/)60 rules covering stability, correctness, performance, and more.
[Go Rules ](/docs/reference/rules/go/)56 rules for goroutine safety, error handling, and production patterns.
[Rust Rules ](/docs/reference/rules/rust/)44 rules for memory safety, async correctness, and panic prevention.
[TypeScript Rules ](/docs/reference/rules/typescript/)35 rules for promise handling, type safety, and Node.js patterns.
## Rules by Dimension
[Section titled “Rules by Dimension”](#rules-by-dimension)
Unfault organizes rules into seven dimensions that map to the qualities that keep systems running reliably:
| Dimension | Focus | Example Rules |
| ------------------- | ------------------------------------------ | ------------------------------------------------- |
| **Stability** | Preventing crashes and service degradation | Timeouts, graceful shutdown, bounded retries |
| **Correctness** | Preventing bugs and data corruption | SQL injection, error handling, type safety |
| **Performance** | Preventing slowdowns | N+1 queries, blocking in async, CPU in event loop |
| **Scalability** | Ensuring systems handle growth | Bounded concurrency, resource limits |
| **Observability** | Improving monitoring and debugging | Structured logging, correlation IDs, tracing |
| **Security** | Preventing vulnerabilities | Hardcoded secrets, unsafe eval, input validation |
| **Maintainability** | Ensuring code quality | Halstead complexity, code duplication |
## Severity Levels
[Section titled “Severity Levels”](#severity-levels)
Each rule is assigned a severity based on its potential impact:
* Critical - Security vulnerabilities or data corruption risks
* High - Can cause outages or significant bugs
* Medium - May cause issues under load or edge cases
* Low - Best practices and code quality
## Auto-Fix Support
[Section titled “Auto-Fix Support”](#auto-fix-support)
Most Unfault rules include auto-fix patches. When Unfault detects a violation, it can generate a diff showing exactly how to fix the issue. Apply patches with:
```bash
unfault review --fix
```
# Go Rules
> All Unfault detection rules for Go code.
Unfault includes **55 rules** for Go, covering core language patterns, goroutine safety, and popular frameworks like Gin, GORM, Echo, gRPC, and Redis.
## Core Rules (43 rules)
[Section titled “Core Rules (43 rules)”](#core-rules-43-rules)
| Rule | Dimension | Severity |
| ------------------------------------------------------------------------------------ | --------------- | -------- |
| [unchecked\_error](/docs/reference/rules/go/unchecked-error/) | Correctness | Medium |
| [defer\_in\_loop](/docs/reference/rules/go/defer-in-loop/) | Performance | Medium |
| [goroutine\_leak](/docs/reference/rules/go/goroutine-leak/) | Stability | High |
| [sql\_injection](/docs/reference/rules/go/sql-injection/) | Security | Critical |
| [http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/) | Stability | High |
| [missing\_structured\_logging](/docs/reference/rules/go/missing-structured-logging/) | Observability | Low |
| [unbounded\_goroutines](/docs/reference/rules/go/unbounded-goroutines/) | Scalability | High |
| [race\_condition](/docs/reference/rules/go/race-condition/) | Correctness | High |
| [hardcoded\_secrets](/docs/reference/rules/go/hardcoded-secrets/) | Security | Critical |
| [type\_assertion\_no\_ok](/docs/reference/rules/go/type-assertion-no-ok/) | Stability | Medium |
| [context\_background](/docs/reference/rules/go/context-background/) | Stability | Medium |
| [halstead\_complexity](/docs/reference/rules/go/halstead-complexity/) | Maintainability | Low |
| [bare\_recover](/docs/reference/rules/go/bare-recover/) | Stability | Medium |
| [channel\_never\_closed](/docs/reference/rules/go/channel-never-closed/) | Stability | High |
| [circuit\_breaker](/docs/reference/rules/go/circuit-breaker/) | Stability | Medium |
| [concurrent\_map\_access](/docs/reference/rules/go/concurrent-map-access/) | Correctness | High |
| [cpu\_in\_hot\_path](/docs/reference/rules/go/cpu-in-hot-path/) | Performance | Medium |
| [empty\_critical\_section](/docs/reference/rules/go/empty-critical-section/) | Correctness | Medium |
| [ephemeral\_filesystem\_write](/docs/reference/rules/go/ephemeral-filesystem-write/) | Stability | Medium |
| [error\_type\_assertion](/docs/reference/rules/go/error-type-assertion/) | Correctness | Medium |
| [global\_mutable\_state](/docs/reference/rules/go/global-mutable-state/) | Correctness | High |
| [graceful\_shutdown](/docs/reference/rules/go/graceful-shutdown/) | Stability | High |
| [http\_retry](/docs/reference/rules/go/http-retry/) | Stability | Medium |
| [idempotency\_key](/docs/reference/rules/go/idempotency-key/) | Correctness | Medium |
| [large\_response\_memory](/docs/reference/rules/go/large-response-memory/) | Scalability | High |
| [map\_without\_size\_hint](/docs/reference/rules/go/map-without-size-hint/) | Performance | Low |
| [missing\_correlation\_id](/docs/reference/rules/go/missing-correlation-id/) | Observability | Medium |
| [missing\_tracing](/docs/reference/rules/go/missing-tracing/) | Observability | Low |
| [panic\_in\_library](/docs/reference/rules/go/panic-in-library/) | Stability | High |
| [rate\_limiting](/docs/reference/rules/go/rate-limiting/) | Scalability | Medium |
| [reflect\_in\_hot\_path](/docs/reference/rules/go/reflect-in-hot-path/) | Performance | Medium |
| [regex\_compile](/docs/reference/rules/go/regex-compile/) | Performance | Low |
| [sentinel\_error\_comparison](/docs/reference/rules/go/sentinel-error-comparison/) | Correctness | Medium |
| [slice\_append\_in\_loop](/docs/reference/rules/go/slice-append-in-loop/) | Performance | Medium |
| [slice\_memory\_leak](/docs/reference/rules/go/slice-memory-leak/) | Stability | High |
| [sync\_dns\_lookup](/docs/reference/rules/go/sync-dns-lookup/) | Performance | Medium |
| [transaction\_boundary](/docs/reference/rules/go/transaction-boundary/) | Correctness | High |
| [unbounded\_cache](/docs/reference/rules/go/unbounded-cache/) | Scalability | High |
| [unbounded\_memory](/docs/reference/rules/go/unbounded-memory/) | Scalability | High |
| [unbounded\_retry](/docs/reference/rules/go/unbounded-retry/) | Stability | High |
| [uncancelled\_context](/docs/reference/rules/go/uncancelled-context/) | Stability | Medium |
| [unhandled\_error\_goroutine](/docs/reference/rules/go/unhandled-error-goroutine/) | Stability | High |
| [unsafe\_template](/docs/reference/rules/go/unsafe-template/) | Security | High |
## Framework Rules
[Section titled “Framework Rules”](#framework-rules)
### Gin (2 rules)
[Section titled “Gin (2 rules)”](#gin-2-rules)
| Rule | Dimension | Severity |
| ----------------------------------------------------------------------- | ----------- | -------- |
| [missing\_validation](/docs/reference/rules/go/gin-missing-validation/) | Correctness | Medium |
| [request\_validation](/docs/reference/rules/go/gin-request-validation/) | Correctness | High |
### Echo (2 rules)
[Section titled “Echo (2 rules)”](#echo-2-rules)
| Rule | Dimension | Severity |
| ------------------------------------------------------------------------ | ----------- | -------- |
| [request\_validation](/docs/reference/rules/go/echo-request-validation/) | Correctness | High |
| [missing\_middleware](/docs/reference/rules/go/echo-missing-middleware/) | Stability | Medium |
### GORM (4 rules)
[Section titled “GORM (4 rules)”](#gorm-4-rules)
| Rule | Dimension | Severity |
| ------------------------------------------------------------------------ | ----------- | -------- |
| [n\_plus\_one](/docs/reference/rules/go/gorm-n-plus-one/) | Performance | High |
| [session\_management](/docs/reference/rules/go/gorm-session-management/) | Stability | High |
| [connection\_pool](/docs/reference/rules/go/gorm-connection-pool/) | Scalability | High |
| [query\_timeout](/docs/reference/rules/go/gorm-query-timeout/) | Stability | High |
### gRPC (1 rule)
[Section titled “gRPC (1 rule)”](#grpc-1-rule)
| Rule | Dimension | Severity |
| -------------------------------------------------------------------- | --------- | -------- |
| [missing\_deadline](/docs/reference/rules/go/grpc-missing-deadline/) | Stability | High |
### Redis (2 rules)
[Section titled “Redis (2 rules)”](#redis-2-rules)
| Rule | Dimension | Severity |
| ------------------------------------------------------------------- | ----------- | -------- |
| [missing\_ttl](/docs/reference/rules/go/redis-missing-ttl/) | Scalability | High |
| [connection\_pool](/docs/reference/rules/go/redis-connection-pool/) | Scalability | High |
### net/http (1 rule)
[Section titled “net/http (1 rule)”](#nethttp-1-rule)
| Rule | Dimension | Severity |
| --------------------------------------------------------------------- | --------- | -------- |
| [missing\_timeout](/docs/reference/rules/go/nethttp-missing-timeout/) | Stability | Critical |
# go.bare_recover
> Detects bare recover() calls that swallow all panics without logging.
Correctness High
Detects `recover()` calls that catch panics without logging or re-panicking, silently hiding errors.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Bare recover calls:
* **Hide bugs** - Panics indicate serious problems that need attention
* **Lose context** - No logs means no way to debug issues
* **Mask failures** - Operations silently fail without notification
* **Delay fixes** - Problems go unnoticed until they cascade
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (swallows panic)
func handler() {
defer func() {
recover() // Panic is silently ignored
}()
riskyOperation()
}
```
```go
// ✅ After (logs and handles)
func handler() {
defer func() {
if r := recover(); r != nil {
log.Error("panic recovered",
"error", r,
"stack", string(debug.Stack()))
// Optionally re-panic or return error
}
}()
riskyOperation()
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `recover()` without checking return value
* `recover()` without logging
* Naked `defer recover()` patterns
* Silent panic suppression
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates patches that add proper panic handling:
```go
defer func() {
if r := recover(); r != nil {
// Log with stack trace
log.Error("panic recovered",
"error", r,
"stack", string(debug.Stack()))
// Convert to error if possible
err = fmt.Errorf("panic: %v", r)
}
}()
```
Caution
Recovering from panics should be rare. Most panics indicate bugs that should be fixed, not suppressed. Use recover only at API boundaries.
## When to Recover
[Section titled “When to Recover”](#when-to-recover)
```go
// HTTP handler - prevent one bad request from crashing server
func panicMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Error("HTTP handler panic", "path", r.URL.Path, "error", r)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
* [go.panic\_in\_library](/docs/reference/rules/go/panic-in-library/)
* [rust.panic\_in\_library](/docs/reference/rules/rust/panic-in-library/)
# go.channel_never_closed
> Detects channels that are created but never closed.
Stability Medium
Detects channels that are created but never closed, causing goroutines waiting on them to leak.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unclosed channels cause:
* **Goroutine leaks** - Receivers block forever waiting
* **Memory leaks** - Blocked goroutines hold references
* **Deadlocks** - Range loops never terminate
* **Resource exhaustion** - Slow accumulation of leaked goroutines
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (channel never closed)
func producer(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
// Channel never closed - consumers block forever!
}
func consumer(ch chan int) {
for v := range ch { // Blocks forever after producer done
process(v)
}
}
```
```go
// ✅ After (channel closed)
func producer(ch chan int) {
defer close(ch)
for i := 0; i < 10; i++ {
ch <- i
}
}
func consumer(ch chan int) {
for v := range ch { // Terminates when channel closed
process(v)
}
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `make(chan T)` without corresponding `close()`
* Channels passed to functions without closing
* Range loops over channels that are never closed
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds `defer close(ch)`:
```go
func produce(ch chan<- int) {
defer close(ch)
// ... produce values
}
```
Tip
Only close channels from the sender side, never from the receiver. Closing a closed channel panics.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
* [go.unbounded\_goroutines](/docs/reference/rules/go/unbounded-goroutines/)
# go.missing_circuit_breaker
> Detects HTTP client calls without circuit breaker protection.
Stability High
Detects HTTP client calls to external services without circuit breaker protection, which can cause cascading failures.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without circuit breakers:
* **Cascading failures** - One slow service brings down everything
* **Resource exhaustion** - Goroutines pile up waiting for responses
* **Extended outages** - Failing service never gets time to recover
* **Poor user experience** - All requests slow down, not just affected ones
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no circuit breaker)
func fetchData(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
return io.ReadAll(resp.Body)
}
```
```go
// ✅ After (with circuit breaker)
import "github.com/sony/gobreaker"
var cb = gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "api",
MaxRequests: 5,
Interval: 10 * time.Second,
Timeout: 30 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures > 5
},
})
func fetchData(url string) ([]byte, error) {
result, err := cb.Execute(func() (interface{}, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
return io.ReadAll(resp.Body)
})
if err != nil {
return nil, err
}
return result.([]byte), nil
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* HTTP calls without circuit breaker wrapper
* gRPC client calls without breakers
* External service integrations without failure isolation
* Database connections without circuit protection
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates patches using `github.com/sony/gobreaker`:
```go
import "github.com/sony/gobreaker"
var circuitBreaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "external-api",
MaxRequests: 3,
Timeout: 60 * time.Second,
})
```
Tip
Circuit breakers have three states: Closed (normal), Open (failing fast), Half-Open (testing recovery). Configure thresholds based on your service’s SLOs.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/)
* [go.unbounded\_retry](/docs/reference/rules/go/unbounded-retry/)
* [python.circuit\_breaker](/docs/reference/rules/python/circuit-breaker/)
# go.concurrent_map_access
> Detects concurrent map access without synchronization.
Correctness Critical
Detects concurrent access to maps without proper synchronization, which causes fatal runtime panics.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Concurrent map access causes:
* **Fatal panics** - Concurrent map read/write is a fatal error in Go
* **Service crashes** - No recovery possible from this panic
* **Intermittent failures** - Only manifests under load
* **Hard to reproduce** - Depends on goroutine timing
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (concurrent access)
var cache = make(map[string]interface{})
func handler(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
cache[key] = "value" // Fatal error if concurrent!
}
```
```go
// ✅ After (sync.Map)
var cache sync.Map
func handler(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
cache.Store(key, "value") // Thread-safe
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Map writes in goroutines
* Map access in HTTP handlers (implicitly concurrent)
* Missing mutex protection on maps
* Maps shared across goroutines
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates thread-safe alternatives:
```go
// Option 1: sync.Map (best for many goroutines, infrequent writes)
var cache sync.Map
// Option 2: RWMutex (best for frequent reads, rare writes)
type SafeMap struct {
mu sync.RWMutex
data map[string]interface{}
}
func (m *SafeMap) Get(key string) interface{} {
m.mu.RLock()
defer m.mu.RUnlock()
return m.data[key]
}
func (m *SafeMap) Set(key string, val interface{}) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = val
}
```
Caution
Use `go run -race` or `go test -race` to detect data races during development. The race detector catches issues that static analysis may miss.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.race\_condition](/docs/reference/rules/go/race-condition/)
* [go.global\_mutable\_state](/docs/reference/rules/go/global-mutable-state/)
# go.context_background
> Detects inappropriate use of context.Background() where proper context should be passed.
Stability Medium
Detects inappropriate use of `context.Background()` where a proper context should be passed.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Context carries cancellation, deadlines, and request-scoped values:
* **Lost cancellation** - Parent cancellation doesn’t propagate
* **Leaked resources** - Operations continue after request ended
* **No deadline propagation** - Request deadline ignored
* **Missing tracing** - Request tracing lost
Using `context.Background()` breaks the context chain that enables Go’s graceful shutdown and timeout patterns.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
// ctx ignored, context.Background() used instead
return s.db.QueryContext(context.Background(), "SELECT...", id)
}
```
If the caller’s context is cancelled, the database query continues running.
```go
// ✅ After
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
return s.db.QueryContext(ctx, "SELECT...", id)
}
```
Now cancellation and deadlines propagate correctly.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `context.Background()` when a context parameter is available
* `context.TODO()` in production code (meant for development)
* HTTP handlers not passing request context
* gRPC handlers ignoring the context parameter
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault replaces `context.Background()` with the available context parameter.
## When Background Is Acceptable
[Section titled “When Background Is Acceptable”](#when-background-is-acceptable)
```go
// Main function initialization
func main() {
ctx := context.Background()
server.Start(ctx)
}
// Long-running background jobs
func StartBackgroundWorker() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
runWorker(ctx)
}
// Tests
func TestSomething(t *testing.T) {
ctx := context.Background()
// ...
}
```
## Best Practices
[Section titled “Best Practices”](#best-practices)
```go
// HTTP handlers: use request context
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.GetUser(r.Context(), userID)
}
// gRPC handlers: context is first parameter
func (s *server) GetUser(ctx context.Context, req *pb.Request) (*pb.Response, error) {
return s.service.GetUser(ctx, req.Id)
}
// Chain context through all calls
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
user, err := s.cache.Get(ctx, id)
if err != nil {
user, err = s.db.Query(ctx, id)
}
return user, err
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
* [go.http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/)
# go.cpu_in_hot_path
> Detects CPU-intensive operations in hot code paths.
Performance Medium
Detects CPU-intensive operations (reflection, regex, JSON marshaling) in frequently-called code paths.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
CPU work in hot paths causes:
* **Increased latency** - P99 latency spikes
* **Reduced throughput** - Less requests per second
* **Higher costs** - Need more instances to handle load
* **Poor scaling** - Performance degrades under load
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (expensive in loop)
func processItems(items []Item) {
for _, item := range items {
json.Marshal(item) // Reflection on every iteration
reflect.TypeOf(item) // Even more expensive
}
}
```
```go
// ✅ After (optimized)
func processItems(items []Item) {
// Batch marshal if needed
data, _ := json.Marshal(items)
// Or pre-compute type information
itemType := reflect.TypeOf(Item{})
for _, item := range items {
// Use cached type info
}
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `reflect` package usage in loops
* JSON marshal/unmarshal in hot paths
* Regex operations in loops (use pre-compiled patterns)
* Hash computations in tight loops
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault suggests moving expensive operations out of loops or pre-computing values.
Tip
Use code generation tools like `easyjson` or `ffjson` to avoid runtime reflection for JSON marshaling in performance-critical paths.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.regex\_compile](/docs/reference/rules/go/regex-compile/)
* [go.reflect\_in\_hot\_path](/docs/reference/rules/go/reflect-in-hot-path/)
* [python.cpu\_in\_event\_loop](/docs/reference/rules/python/cpu-in-event-loop/)
# go.defer_in_loop
> Detects defer statements inside loops.
Performance Medium
Detects `defer` statements inside loops.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Deferred calls accumulate until the function returns, not until the loop iteration ends:
* **Resource exhaustion** - File handles accumulate until function exits
* **Memory leak** - Deferred closures hold references
* **Crash under load** - Works fine with 10 items, crashes with 10,000
* **Counter-intuitive** - Common misconception even among experienced Go devs
This is one of Go’s most common gotchas.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close() // Doesn't close until function returns!
// Process file...
}
```
If you have 10,000 files, you open 10,000 file handles before closing any.
```go
// ✅ After (closure)
for _, file := range files {
func() {
f, err := os.Open(file)
if err != nil {
return
}
defer f.Close()
// Process file...
}()
}
```
```go
// ✅ After (explicit close)
for _, file := range files {
f, err := os.Open(file)
if err != nil {
return err
}
// Process file...
f.Close()
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `defer` inside `for` loops
* `defer` inside `range` loops
* Nested loop defer patterns
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault wraps the loop body in an immediately-invoked function literal (IIFE).
## Best Practices
[Section titled “Best Practices”](#best-practices)
```go
// Extract to helper function
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// Process...
return nil
}
for _, file := range files {
if err := processFile(file); err != nil {
return err
}
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
# go.echo.missing_middleware
> Detects Echo apps without essential middleware.
Stability Medium
Detects Echo apps without essential middleware.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing middleware:
* **No error handling** - Unhandled panics crash server
* **No request logging** - Can’t debug issues
* **No request timeout** - Slow requests block server
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no middleware)
e := echo.New()
e.GET("/", handler)
```
```go
// ✅ After (with essential middleware)
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Timeout: 30 * time.Second,
}))
e.GET("/", handler)
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Missing Recover middleware
* Missing Logger middleware
* Missing Timeout middleware
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.graceful\_shutdown](/docs/reference/rules/go/graceful-shutdown/)
# go.echo.request_validation
> Detects Echo handlers without request validation.
Correctness Medium
Detects Echo handlers without request validation.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing validation:
* **Invalid data accepted** - Bad input reaches business logic
* **Security vulnerabilities** - Unvalidated input exploitable
* **Runtime errors** - Type mismatches cause panics
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no validation)
func CreateUser(c echo.Context) error {
var req UserRequest
c.Bind(&req) // No validation!
return createUser(req)
}
```
```go
// ✅ After (with validation)
func CreateUser(c echo.Context) error {
var req UserRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := c.Validate(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return createUser(req)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Bind() without Validate()
* Missing validation on request body
* No error handling on bind
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.gin.missing\_validation](/docs/reference/rules/go/gin-missing-validation/)
# go.empty_critical_section
> Detects mutex locks with empty or trivial critical sections.
Performance Medium
Detects mutex locks protecting empty or trivial critical sections, causing unnecessary contention.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Empty critical sections cause:
* **Unnecessary contention** - Goroutines block for nothing
* **Deadlock risk** - Complex lock patterns with no benefit
* **Performance degradation** - Lock overhead without protection
* **Code smell** - Often indicates incomplete implementations
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (pointless locking)
var mu sync.Mutex
func process() {
mu.Lock()
defer mu.Unlock()
// Nothing protected!
}
// Also problematic
func update() {
mu.Lock()
mu.Unlock() // Immediate unlock
doWork() // Work happens outside lock
}
```
```go
// ✅ After (meaningful critical section)
var mu sync.Mutex
var counter int
func process() {
mu.Lock()
defer mu.Unlock()
counter++ // Actually protected
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `Lock()`/`Unlock()` with nothing between them
* `defer mu.Unlock()` with no protected operations
* Critical sections with only logging
* Lock/unlock without shared state access
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault flags these for manual review since the fix depends on intent:
```go
// Option 1: Remove unnecessary lock
func process() {
doWork() // If no shared state
}
// Option 2: Add protected operations
func process() {
mu.Lock()
sharedData = newValue
mu.Unlock()
doWork()
}
```
Tip
If you’re using locks for synchronization (not data protection), consider using `sync.WaitGroup`, channels, or `sync.Cond` instead.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.race\_condition](/docs/reference/rules/go/race-condition/)
* [go.concurrent\_map\_access](/docs/reference/rules/go/concurrent-map-access/)
# go.ephemeral_filesystem_write
> Detects filesystem writes that may be lost in containers.
Stability Medium
Detects filesystem writes to ephemeral locations in containerized environments.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Ephemeral writes:
* **Lost on restart** - Container restarts lose local files
* **Not shared** - Multiple instances don’t share files
* **Lost on scale** - New pods don’t have the data
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (ephemeral)
func saveFile(data []byte) error {
return os.WriteFile("/tmp/data.json", data, 0644)
}
```
```go
// ✅ After (persistent storage)
func saveFile(ctx context.Context, data []byte) error {
// Use S3 or mounted volume
_, err := s3Client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String("my-bucket"),
Key: aws.String("data.json"),
Body: bytes.NewReader(data),
})
return err
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `os.WriteFile()` to local paths
* `os.Create()` for persistent data
* `ioutil.WriteFile()` in containerized apps
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.ephemeral\_filesystem\_write](/docs/reference/rules/python/ephemeral-filesystem-write/)
# go.error_type_assertion
> Detects error type assertions without using errors.As().
Correctness Medium
Detects error type assertions using direct type assertion instead of `errors.As()`.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Direct type assertions on errors:
* **Miss wrapped errors** - Wrapped errors don’t match direct type checks
* **Break error chains** - Go 1.13+ error wrapping is ignored
* **Lose error context** - Miss errors wrapped with `fmt.Errorf("%w")`
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (misses wrapped errors)
if e, ok := err.(*MyError); ok {
handleMyError(e)
}
```
```go
// ✅ After (handles wrapped errors)
var myErr *MyError
if errors.As(err, &myErr) {
handleMyError(myErr)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `err.(*Type)` type assertions
* `switch err.(type)` without unwrapping
* Missing `errors.As()` for custom error types
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault replaces type assertions with `errors.As()`:
```go
var e *MyError
if errors.As(err, &e) {
// ...
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.sentinel\_error\_comparison](/docs/reference/rules/go/sentinel-error-comparison/)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
# go.gin.missing_validation
> Detects Gin handlers that bind request data without validation tags.
Correctness Medium
Detects Gin handlers that bind request data without validation tags.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unvalidated input causes problems:
* **Invalid data accepted** - Garbage in, garbage out
* **Null pointer panics** - Missing required fields cause crashes
* **Business logic errors** - Constraints violated downstream
* **Security vulnerabilities** - Unexpected input exploits assumptions
Gin has built-in validation through struct tags - use it.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
type CreateUserRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Age int `json:"age"`
}
func CreateUser(c *gin.Context) {
var req CreateUserRequest
c.ShouldBindJSON(&req) // No validation
// Empty email? Negative age? Anything goes.
}
```
```go
// ✅ After
type CreateUserRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Age int `json:"age" binding:"gte=0,lte=150"`
}
func CreateUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Email is valid, password has 8+ chars, age is reasonable
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Struct bindings without `binding:` tags
* Missing `required` on essential fields
* ShouldBind without error checking
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds common validation tags based on field names and types.
## Common Validation Tags
[Section titled “Common Validation Tags”](#common-validation-tags)
```go
type Request struct {
// Required fields
Name string `binding:"required"`
// String constraints
Email string `binding:"required,email"`
URL string `binding:"url"`
UUID string `binding:"uuid"`
// Numeric constraints
Age int `binding:"gte=0,lte=150"`
Count int `binding:"min=1,max=100"`
// Length constraints
Password string `binding:"min=8,max=128"`
Code string `binding:"len=6"`
// Enums
Status string `binding:"oneof=pending active done"`
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
# go.gin.request_validation
> Detects Gin handlers without request validation.
Correctness High
Detects Gin handlers without request validation.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing request validation:
* **Invalid data** - Malformed input causes errors
* **Security risks** - Unvalidated input enables attacks
* **Poor UX** - Users get cryptic errors
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no validation)
func CreateUser(c *gin.Context) {
var user User
c.BindJSON(&user) // No error handling!
// process user...
}
```
```go
// ✅ After (with validation)
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=1,max=100"`
Email string `json:"email" binding:"required,email"`
}
func CreateUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// process validated request...
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* BindJSON without error handling
* Missing struct validation tags
* Handlers without request binding
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add proper ShouldBind patterns with error handling.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.echo.request\_validation](/docs/reference/rules/go/echo-request-validation/)
# go.global_mutable_state
> Detects global mutable variables that can cause race conditions.
Correctness High
Detects global mutable state that can cause race conditions in concurrent code.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Global mutable state causes:
* **Race conditions** - Concurrent access without synchronization
* **Testing difficulties** - Tests affect each other
* **Hidden dependencies** - Functions have implicit state
* **Unpredictable behavior** - State changes unexpectedly
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unsafe global state)
var cache = make(map[string]interface{})
func Get(key string) interface{} {
return cache[key] // Race condition!
}
func Set(key string, value interface{}) {
cache[key] = value // Race condition!
}
```
```go
// ✅ After (thread-safe)
var cache sync.Map
func Get(key string) (interface{}, bool) {
return cache.Load(key)
}
func Set(key string, value interface{}) {
cache.Store(key, value)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Package-level `var` with mutable types (maps, slices)
* Global variables accessed from multiple goroutines
* Missing mutex protection on shared state
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates thread-safe alternatives:
```go
// Option 1: sync.Map for concurrent maps
var cache sync.Map
// Option 2: Mutex-protected struct
type SafeCache struct {
mu sync.RWMutex
data map[string]interface{}
}
func (c *SafeCache) Get(key string) interface{} {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
```
Tip
Prefer dependency injection over global state. Pass state explicitly to make dependencies clear and testing easier.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.race\_condition](/docs/reference/rules/go/race-condition/)
* [go.concurrent\_map\_access](/docs/reference/rules/go/concurrent-map-access/)
* [python.global\_mutable\_state](/docs/reference/rules/python/global-mutable-state/)
# go.gorm.connection_pool
> Detects missing or misconfigured GORM connection pool settings.
Scalability High
Detects missing or misconfigured GORM connection pool settings.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing connection pool configuration:
* **Connection exhaustion** - Too many connections overwhelm database
* **Resource waste** - Idle connections consume memory
* **Timeouts** - No connection reuse leads to slow queries
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no pool configuration)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
```
```go
// ✅ After (with connection pool settings)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
sqlDB, err := db.DB()
if err != nil {
log.Fatal(err)
}
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Missing SetMaxOpenConns
* Missing SetMaxIdleConns
* Missing SetConnMaxLifetime
* Unreasonable pool size values
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add appropriate connection pool configuration based on common best practices.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.gorm.session\_management](/docs/reference/rules/go/gorm-session-management/)
* [go.gorm.query\_timeout](/docs/reference/rules/go/gorm-query-timeout/)
# go.gorm.n_plus_one
> Detects N+1 query patterns in GORM code.
Performance High
Detects N+1 query patterns in GORM code.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
N+1 queries:
* **Performance degradation** - Linear query growth per record
* **Database overload** - Excessive roundtrips saturate connections
* **Latency spikes** - Each query adds network overhead
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (N+1 queries)
var users []User
db.Find(&users)
for _, user := range users {
var orders []Order
db.Where("user_id = ?", user.ID).Find(&orders) // Query per user!
user.Orders = orders
}
```
```go
// ✅ After (eager loading with Preload)
var users []User
db.Preload("Orders").Find(&users)
```
```go
// ✅ Alternative (using Joins)
var users []User
db.Joins("Orders").Find(&users)
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Queries inside loops accessing related data
* Missing Preload for associations
* Repeated queries with only ID varying
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can suggest Preload patterns for detected N+1 queries.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.gorm.query\_timeout](/docs/reference/rules/go/gorm-query-timeout/)
* [go.transaction\_boundary](/docs/reference/rules/go/transaction-boundary/)
# go.gorm.query_timeout
> Detects GORM queries without timeout configuration.
Stability High
Detects GORM queries without timeout configuration.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Queries without timeouts:
* **Resource blocking** - Slow queries hold connections indefinitely
* **Cascading failures** - Database issues propagate to application
* **Poor user experience** - Requests hang without feedback
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no query timeout)
func GetUser(id uint) (*User, error) {
var user User
err := db.First(&user, id).Error
return &user, err
}
```
```go
// ✅ After (with context timeout)
func GetUser(ctx context.Context, id uint) (*User, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var user User
err := db.WithContext(ctx).First(&user, id).Error
return &user, err
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* GORM queries without context
* Missing WithContext calls
* Hardcoded long timeout values
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can wrap queries with context timeout patterns.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_timeout](/docs/reference/rules/go/http-timeout/)
* [go.gorm.connection\_pool](/docs/reference/rules/go/gorm-connection-pool/)
# go.gorm.session_management
> Detects improper GORM session management patterns.
Stability High
Detects improper GORM session management patterns.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Improper session management:
* **Connection leaks** - Sessions not closed properly
* **Stale data** - Reusing cached session state
* **Race conditions** - Sharing sessions across goroutines
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (reusing global db instance unsafely)
var db *gorm.DB
func GetUser(id uint) User {
var user User
db.First(&user, id) // May have stale session state
return user
}
```
```go
// ✅ After (using fresh session)
var db *gorm.DB
func GetUser(id uint) User {
var user User
db.Session(&gorm.Session{}).First(&user, id)
return user
}
// Or with context
func GetUserWithContext(ctx context.Context, id uint) User {
var user User
db.WithContext(ctx).First(&user, id)
return user
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Sharing DB instances across goroutines without session isolation
* Missing WithContext in request handlers
* Stale session reuse patterns
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.gorm.connection\_pool](/docs/reference/rules/go/gorm-connection-pool/)
* [go.transaction\_boundary](/docs/reference/rules/go/transaction-boundary/)
# go.goroutine_leak
> Detects goroutines that may never terminate.
Stability High Causes Production Outages
Detects goroutines that may never terminate (blocking on channels without cancellation).
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Leaked goroutines accumulate over time:
* **Memory growth** - Each goroutine uses \~2KB stack minimum
* **Resource exhaustion** - Eventually crashes the application
* **Hard to detect** - No obvious symptoms until it’s too late
* **Slow degradation** - Performance degrades gradually
A goroutine leak is like a memory leak, but worse because goroutines also hold references to other resources.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
func startWorker() {
go func() {
for {
msg := <-messages // Blocks forever if channel closes
process(msg)
}
}()
}
```
If `messages` is never closed and nothing sends, this goroutine lives forever.
```go
// ✅ After
func startWorker(ctx context.Context) {
go func() {
for {
select {
case msg := <-messages:
process(msg)
case <-ctx.Done():
return
}
}
}()
}
```
The goroutine exits when context is cancelled.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Goroutines blocking on channel receive without `select`
* Goroutines without cancellation mechanism
* Channel sends without corresponding receives
* Forever loops without exit conditions
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add context-based cancellation to goroutine patterns when the blocking pattern is clearly identified.
Note
Unfault focuses on common patterns like channel receives without select and infinite loops. Complex concurrency patterns may require manual review.
## Common Leak Patterns
[Section titled “Common Leak Patterns”](#common-leak-patterns)
```go
// LEAK: Unbuffered channel with no receiver
ch := make(chan int)
go func() {
ch <- 1 // Blocks forever
}()
// LEAK: Range over channel never closed
go func() {
for v := range ch { // Blocks forever at end
process(v)
}
}()
// LEAK: Select without done case
go func() {
select {
case v := <-ch:
process(v)
} // Only processes once, then exits - but what if ch never sends?
}()
```
## Safe Patterns
[Section titled “Safe Patterns”](#safe-patterns)
```go
// Timeout
select {
case v := <-ch:
process(v)
case <-time.After(30 * time.Second):
return // Give up after timeout
}
// Context cancellation
select {
case v := <-ch:
process(v)
case <-ctx.Done():
return // Cancelled by parent
}
// Buffered channels for fire-and-forget
ch := make(chan int, 1)
ch <- 1 // Doesn't block
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_goroutines](/docs/reference/rules/go/unbounded-goroutines/)
* [go.context\_background](/docs/reference/rules/go/context-background/)
# go.missing_graceful_shutdown
> Detects HTTP servers without graceful shutdown handling.
Stability High
Detects HTTP servers that don’t handle SIGTERM for graceful shutdown, causing dropped requests during deployments.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without graceful shutdown:
* **Dropped requests** - In-flight requests are terminated mid-processing
* **Data loss** - Partial writes, uncommitted transactions
* **Connection errors** - Clients receive connection reset errors
* **Deployment failures** - Rolling updates cause user-visible errors
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no graceful shutdown)
func main() {
http.ListenAndServe(":8080", handler)
}
```
```go
// ✅ After (graceful shutdown)
func main() {
server := &http.Server{
Addr: ":8080",
Handler: handler,
}
// Start server in goroutine
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
log.Println("Server exited gracefully")
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `http.ListenAndServe()` without signal handling
* Missing `server.Shutdown()` calls
* `log.Fatal()` in signal handlers (prevents cleanup)
* Hardcoded exits without cleanup
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates patches that add signal handling and graceful shutdown:
```go
import (
"context"
"os"
"os/signal"
"syscall"
)
// Graceful shutdown setup
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
```
Tip
Set Kubernetes `terminationGracePeriodSeconds` to match or exceed your server’s shutdown timeout to avoid SIGKILL during deployments.
## Kubernetes Configuration
[Section titled “Kubernetes Configuration”](#kubernetes-configuration)
```yaml
spec:
terminationGracePeriodSeconds: 30
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/)
* [python.graceful\_shutdown](/docs/reference/rules/python/graceful-shutdown/)
* [rust.tokio.missing\_graceful\_shutdown](/docs/reference/rules/rust/tokio-missing-graceful-shutdown/)
# go.grpc.missing_deadline
> Detects gRPC calls without deadline or timeout.
Stability Critical
Detects gRPC calls without deadline or timeout.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
gRPC calls without deadlines:
* **Resource exhaustion** - Hanging calls consume connections
* **Cascading failures** - Slow services block callers indefinitely
* **No failure feedback** - Clients wait forever
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no deadline)
resp, err := client.GetUser(context.Background(), req)
```
```go
// ✅ After (with deadline)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, req)
if err != nil {
if status.Code(err) == codes.DeadlineExceeded {
// Handle timeout
}
return nil, err
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* gRPC client calls with context.Background()
* Missing context.WithTimeout or WithDeadline
* Unreasonably long timeout values
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can wrap gRPC calls with appropriate timeout contexts.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_timeout](/docs/reference/rules/go/http-timeout/)
* [go.context\_background](/docs/reference/rules/go/context-background/)
# go.halstead_complexity
> Analyzes Halstead complexity metrics to identify hard-to-maintain functions.
Maintainability Low
Analyzes Halstead complexity metrics to identify hard-to-maintain functions.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
High complexity functions have measurable consequences:
* **More bugs** - Studies show defect density increases with complexity
* **Slower development** - Takes longer to understand and modify
* **Testing burden** - More paths require more test cases
* **Review difficulty** - Code reviewers miss issues in complex code
Halstead metrics provide objective measures that correlate with maintenance cost.
## Halstead Metrics
[Section titled “Halstead Metrics”](#halstead-metrics)
* **Difficulty (D)** - How hard to write or understand
* **Effort (E)** - Mental effort required
* **Volume (V)** - Information content
* **Vocabulary (n)** - Unique operators and operands
* **Length (N)** - Total operators and operands
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (high complexity)
func processOrder(order *Order, user *User, inv *Inventory, pay *Payment) error {
if order.Status != "pending" {
return errors.New("invalid status")
}
if !user.Verified {
return errors.New("user not verified")
}
if user.Balance < order.Total {
return errors.New("insufficient balance")
}
for _, item := range order.Items {
stock, ok := inv.Stock[item.ID]
if !ok || stock < item.Qty {
return fmt.Errorf("item %s out of stock", item.ID)
}
inv.Stock[item.ID] -= item.Qty
if inv.Stock[item.ID] < 10 {
notifyRestock(item.ID)
}
}
if err := pay.Charge(user.ID, order.Total); err != nil {
for _, item := range order.Items {
inv.Stock[item.ID] += item.Qty // Rollback
}
return err
}
order.Status = "complete"
return nil
}
```
```go
// ✅ After (decomposed)
func processOrder(ctx *OrderContext) error {
if err := validateOrder(ctx.Order, ctx.User); err != nil {
return err
}
reserved, err := reserveInventory(ctx.Order, ctx.Inventory)
if err != nil {
return err
}
if err := processPayment(ctx.User, ctx.Order.Total, ctx.Payment); err != nil {
releaseInventory(reserved, ctx.Inventory)
return err
}
ctx.Order.Status = "complete"
return nil
}
func validateOrder(order *Order, user *User) error {
if order.Status != "pending" {
return errors.New("invalid status")
}
if !user.Verified {
return errors.New("user not verified")
}
if user.Balance < order.Total {
return errors.New("insufficient balance")
}
return nil
}
```
Each function has a single responsibility and lower complexity.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Functions exceeding difficulty threshold
* High effort scores
* Deeply nested control flow
* Long parameter lists
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault flags high-complexity functions. Refactoring is manual but guided.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.halstead\_complexity](/docs/reference/rules/python/halstead-complexity/)
* [rust.halstead\_complexity](/docs/reference/rules/rust/halstead-complexity/)
* [typescript.halstead\_complexity](/docs/reference/rules/typescript/halstead-complexity/)
# go.hardcoded_secrets
> Detects hardcoded API keys, passwords, and secrets in source code.
Security Critical Common in Incidents
Detects hardcoded API keys, passwords, and other secrets in source code.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Hardcoded secrets are a severe security risk:
* **Version control exposure** - Secrets committed to git are visible to everyone with access
* **Cannot rotate** - Changing a secret requires code changes and deployment
* **Audit impossible** - No way to track secret access
* **Breach amplification** - One compromised repo exposes all services
Secrets in code get leaked through backups, logs, error messages, and repository access.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
const (
APIKey = "sk_live_abc123xyz"
Password = "supersecret"
DatabaseURL = "postgres://user:pass@host/db"
)
client := stripe.NewClient("sk_live_abc123xyz")
```
```go
// ✅ After
import "os"
var (
APIKey = os.Getenv("STRIPE_API_KEY")
Password = os.Getenv("DB_PASSWORD")
DatabaseURL = os.Getenv("DATABASE_URL")
)
client := stripe.NewClient(os.Getenv("STRIPE_API_KEY"))
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Variables named `password`, `secret`, `key`, `token`, etc.
* Strings matching API key patterns (AWS, Stripe, GitHub, etc.)
* Database connection strings with credentials
* JWT secrets and signing keys
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can replace hardcoded values with `os.Getenv()` calls when the pattern is recognized.
Note
Unfault uses pattern matching for known API key formats and sensitive variable names. Test data and clearly marked example values may still be flagged for review.
## Best Practices
[Section titled “Best Practices”](#best-practices)
```go
// Use environment variables
apiKey := os.Getenv("API_KEY")
if apiKey == "" {
log.Fatal("API_KEY environment variable required")
}
// Or use a secrets manager
import "github.com/aws/aws-sdk-go/service/secretsmanager"
secret, err := sm.GetSecretValue(&secretsmanager.GetSecretValueInput{
SecretId: aws.String("my-secret"),
})
// Configuration libraries
import "github.com/spf13/viper"
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
apiKey := viper.GetString("API_KEY")
```
## Common Secret Patterns
[Section titled “Common Secret Patterns”](#common-secret-patterns)
| Pattern | Example |
| -------------- | ---------------------------- |
| AWS Access Key | `AKIA...` |
| AWS Secret Key | 40-char base64 |
| Stripe Key | `sk_live_...`, `pk_live_...` |
| GitHub Token | `ghp_...`, `github_pat_...` |
| JWT Secret | Long random strings |
| Database URL | `postgres://user:pass@...` |
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [rust.hardcoded\_secrets](/docs/reference/rules/rust/hardcoded-secrets/)
* [typescript.hardcoded\_secrets](/docs/reference/rules/typescript/hardcoded-secrets/)
# go.http_missing_timeout
> Detects HTTP client usage without timeout configuration.
Stability High Common in Incidents
Detects HTTP client usage without explicit timeout configuration.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
HTTP requests without timeouts can hang indefinitely:
* **Connection pool exhaustion** - Stuck requests hold connections
* **Goroutine leaks** - Waiting goroutines accumulate
* **Cascade failures** - One slow upstream brings down your service
* **Unresponsive service** - All workers blocked waiting
The default `http.Client` has no timeout. This is a dangerous default.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
client := &http.Client{}
resp, err := client.Get(url)
// Also bad: using http.Get directly
resp, err := http.Get(url)
```
If the server never responds, these calls wait forever.
```go
// ✅ After
client := &http.Client{
Timeout: 30 * time.Second,
}
resp, err := client.Get(url)
```
After 30 seconds, the request fails with a timeout error.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `&http.Client{}` without Timeout field
* `http.Get()`, `http.Post()`, etc. (use default client)
* Client with Transport but no timeout
* Missing context deadline on requests
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add `Timeout: 30 * time.Second` to HTTP client initialization when configuring a default client.
Note
Unfault detects `&http.Client{}` without Timeout and direct usage of `http.Get()`. Clients using custom Transport configurations are flagged but may need manual timeout tuning.
## Best Practices
[Section titled “Best Practices”](#best-practices)
```go
// Overall request timeout
client := &http.Client{
Timeout: 30 * time.Second,
}
// Fine-grained control with Transport
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second, // Connection timeout
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
IdleConnTimeout: 90 * time.Second,
},
}
// Context-based timeout (per-request)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := client.Do(req)
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.http.missing\_timeout](/docs/reference/rules/python/http-missing-timeout/)
* [typescript.http\_missing\_timeout](/docs/reference/rules/typescript/http-missing-timeout/)
* [go.grpc.missing\_deadline](/docs/reference/rules/go/grpc-missing-deadline/)
# go.http_retry
> Detects HTTP calls without retry logic for transient failures.
Stability Medium
Detects HTTP client calls without retry logic, which fail on transient network issues.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without retries:
* **Transient failures become permanent** - Network blips cause request failures
* **Poor user experience** - Users see errors for recoverable issues
* **Reduced reliability** - 99.9% uptime requires handling temporary failures
* **Cascading issues** - One failed request may abort entire workflows
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no retry)
func fetchUser(id string) (*User, error) {
resp, err := http.Get(fmt.Sprintf("/users/%s", id))
if err != nil {
return nil, err // Fails on first transient error
}
// ...
}
```
```go
// ✅ After (with retry)
import "github.com/hashicorp/go-retryablehttp"
var client = retryablehttp.NewClient()
func fetchUser(id string) (*User, error) {
resp, err := client.Get(fmt.Sprintf("/users/%s", id))
if err != nil {
return nil, err
}
// ...
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* HTTP GET/POST without retry wrapper
* Missing retry on 5xx responses
* Missing retry on connection errors
* Direct `http.Client` usage without retry policy
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates patches using `go-retryablehttp`:
```go
import "github.com/hashicorp/go-retryablehttp"
client := retryablehttp.NewClient()
client.RetryMax = 3
client.RetryWaitMin = 1 * time.Second
client.RetryWaitMax = 30 * time.Second
```
Caution
Only retry idempotent operations (GET, PUT, DELETE). Retrying POST without idempotency keys can cause duplicate operations.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_retry](/docs/reference/rules/go/unbounded-retry/)
* [go.circuit\_breaker](/docs/reference/rules/go/circuit-breaker/)
* [go.http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/)
# go.idempotency_key
> Detects POST/PUT endpoints lacking idempotency key handling.
Correctness Medium
Detects state-modifying HTTP endpoints without idempotency key handling.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without idempotency keys:
* **Duplicate operations** - Retries create duplicate records
* **Double charges** - Payment retries charge multiple times
* **Data inconsistency** - Same request processed multiple times
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no idempotency)
func createOrder(w http.ResponseWriter, r *http.Request) {
order := parseOrder(r)
db.Create(&order) // Duplicate on retry!
}
```
```go
// ✅ After (with idempotency key)
func createOrder(w http.ResponseWriter, r *http.Request) {
idempotencyKey := r.Header.Get("Idempotency-Key")
if idempotencyKey == "" {
http.Error(w, "Idempotency-Key required", 400)
return
}
// Check if already processed
if result, ok := cache.Get(idempotencyKey); ok {
json.NewEncoder(w).Encode(result)
return
}
order := parseOrder(r)
db.Create(&order)
cache.Set(idempotencyKey, order, 24*time.Hour)
json.NewEncoder(w).Encode(order)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* POST/PUT handlers without idempotency key checks
* Payment processing without idempotency
* Order creation without duplicate protection
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates idempotency middleware:
```go
func IdempotencyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" || r.Method == "PUT" {
key := r.Header.Get("Idempotency-Key")
// Check/store key...
}
next.ServeHTTP(w, r)
})
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.idempotency\_key](/docs/reference/rules/python/idempotency-key/)
# go.large_response_memory
> Detects unbounded response body reads that can exhaust memory.
Stability Medium
Detects HTTP response body reads without size limits, which can exhaust memory.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unbounded response reads cause:
* **OOM crashes** - Malicious or broken servers return huge responses
* **DoS vulnerability** - Attackers control response size
* **Resource exhaustion** - Memory spikes affect other requests
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unbounded)
resp, _ := http.Get(url)
body, _ := io.ReadAll(resp.Body) // Could be gigabytes!
```
```go
// ✅ After (bounded)
resp, _ := http.Get(url)
body, _ := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) // 10MB max
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `io.ReadAll(resp.Body)` without limits
* `ioutil.ReadAll()` without Content-Length check
* JSON decoding without size limits
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds `io.LimitReader`:
```go
const maxResponseSize = 10 * 1024 * 1024 // 10MB
body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_memory](/docs/reference/rules/go/unbounded-memory/)
* [python.large\_response\_memory](/docs/reference/rules/python/large-response-memory/)
# go.map_without_size_hint
> Detects map initialization without size hint when size is known.
Performance Low
Detects map initialization without capacity hint when the expected size is known.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Maps without size hints:
* **Repeated rehashing** - Map grows and rehashes as items added
* **Memory copying** - Each grow copies all entries
* **Fragmented memory** - Multiple allocations instead of one
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no hint)
result := make(map[string]int)
for _, item := range items {
result[item.Key] = item.Value
}
```
```go
// ✅ After (with hint)
result := make(map[string]int, len(items))
for _, item := range items {
result[item.Key] = item.Value
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `make(map[K]V)` in loops where size is known
* Map initialization followed by loop filling it
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds size hints:
```go
result := make(map[string]int, len(items))
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.slice\_append\_in\_loop](/docs/reference/rules/go/slice-append-in-loop/)
# go.missing_correlation_id
> Detects HTTP handlers without correlation ID propagation.
Observability Low
Detects HTTP handlers that don’t propagate correlation IDs for distributed tracing.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without correlation IDs:
* **Can’t trace requests** - Logs across services can’t be correlated
* **Debugging nightmare** - Finding related events is manual work
* **Slow incident response** - More time spent correlating logs
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no correlation ID)
func handler(w http.ResponseWriter, r *http.Request) {
log.Println("Processing request")
}
```
```go
// ✅ After (with correlation ID)
func handler(w http.ResponseWriter, r *http.Request) {
correlationID := r.Header.Get("X-Correlation-ID")
if correlationID == "" {
correlationID = uuid.New().String()
}
ctx := context.WithValue(r.Context(), "correlation_id", correlationID)
log.Printf("Processing request correlation_id=%s", correlationID)
w.Header().Set("X-Correlation-ID", correlationID)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* HTTP handlers without correlation ID extraction
* Log statements without correlation IDs
* Outgoing requests without ID forwarding
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.missing\_structured\_logging](/docs/reference/rules/go/missing-structured-logging/)
* [go.missing\_tracing](/docs/reference/rules/go/missing-tracing/)
* [python.missing\_correlation\_id](/docs/reference/rules/python/missing-correlation-id/)
# go.missing_structured_logging
> Detects usage of fmt.Println or log.Print instead of structured logging.
Observability Low
Detects usage of `fmt.Println`/`log.Print` instead of structured logging (zerolog/zap/slog).
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unstructured logs are hard to work with:
* **Not queryable** - Can’t search for specific fields
* **Not aggregatable** - Can’t count or group events
* **Not alertable** - Can’t set conditions on values
* **Parsing hell** - Regex extraction is fragile
Modern observability requires structured data with consistent fields.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
fmt.Printf("User %s logged in\n", userID)
log.Printf("Error: %v", err)
```
These are impossible to filter or aggregate in your logging system.
```go
// ✅ After (zerolog)
import "github.com/rs/zerolog/log"
log.Info().Str("user_id", userID).Msg("user logged in")
log.Error().Err(err).Msg("operation failed")
```
Now you can query `user_id:123` or count login events by user.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `fmt.Print`, `fmt.Printf`, `fmt.Println`
* `log.Print`, `log.Printf`, `log.Println`
* `log.Fatal`, `log.Panic` (also replace with structured)
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault converts print statements to zerolog calls.
## Recommended Libraries
[Section titled “Recommended Libraries”](#recommended-libraries)
```go
// zerolog - Fast, structured, zero allocation
import "github.com/rs/zerolog/log"
log.Info().Str("key", "value").Msg("message")
// zap - Uber's high-performance logger
import "go.uber.org/zap"
logger, _ := zap.NewProduction()
logger.Info("message", zap.String("key", "value"))
// slog - Standard library (Go 1.21+)
import "log/slog"
slog.Info("message", "key", "value")
```
## Configuration
[Section titled “Configuration”](#configuration)
```go
// zerolog with pretty console for dev
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
// Production JSON output
log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.missing\_structured\_logging](/docs/reference/rules/python/missing-structured-logging/)
* [typescript.console\_in\_production](/docs/reference/rules/typescript/console-in-production/)
# go.missing_tracing
> Detects code without distributed tracing instrumentation.
Observability Low
Detects code without distributed tracing instrumentation for observability.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without tracing:
* **No visibility** - Can’t see request flow across services
* **Slow debugging** - Can’t identify bottlenecks
* **Missing metrics** - No latency data per operation
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no tracing)
func handleRequest(ctx context.Context) error {
data := fetchData(ctx)
return processData(data)
}
```
```go
// ✅ After (with OpenTelemetry)
import "go.opentelemetry.io/otel"
var tracer = otel.Tracer("my-service")
func handleRequest(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "handleRequest")
defer span.End()
data := fetchData(ctx)
return processData(ctx, data)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* HTTP handlers without trace spans
* Database operations without spans
* Service calls without trace propagation
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.missing\_structured\_logging](/docs/reference/rules/go/missing-structured-logging/)
* [go.missing\_correlation\_id](/docs/reference/rules/go/missing-correlation-id/)
* [python.missing\_tracing](/docs/reference/rules/python/missing-tracing/)
# go.nethttp.missing_timeout
> Detects net/http clients and servers without timeout configuration.
Stability Critical
Detects net/http clients and servers without timeout configuration.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing HTTP timeouts:
* **Resource exhaustion** - Hanging connections consume resources
* **Slowloris attacks** - Servers vulnerable to slow clients
* **Cascading failures** - Slow dependencies block callers
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no timeout - client)
client := &http.Client{}
resp, err := client.Get("https://api.example.com/data")
// ❌ Before (no timeout - server)
server := &http.Server{
Addr: ":8080",
}
```
```go
// ✅ After (with timeout - client)
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
}).DialContext,
ResponseHeaderTimeout: 10 * time.Second,
IdleConnTimeout: 90 * time.Second,
},
}
// ✅ After (with timeout - server)
server := &http.Server{
Addr: ":8080",
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* http.Client without Timeout
* http.Server without ReadTimeout/WriteTimeout
* http.Transport without timeouts
* Use of http.DefaultClient
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add appropriate timeout configuration to HTTP clients and servers.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_timeout](/docs/reference/rules/go/http-timeout/)
* [go.graceful\_shutdown](/docs/reference/rules/go/graceful-shutdown/)
# go.panic_in_library
> Detects panic() calls in library code that should return errors.
Stability High
Detects `panic()` calls in library code that should return errors instead.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Panics in libraries:
* **Crash calling applications** - Library panic terminates the app
* **Remove caller control** - Callers can’t handle errors gracefully
* **Violate Go conventions** - Libraries should return errors
* **Cause production outages** - One bad input crashes everything
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (panic in library)
func Parse(data []byte) Config {
if len(data) == 0 {
panic("empty data") // Crashes caller!
}
// ...
}
```
```go
// ✅ After (returns error)
func Parse(data []byte) (Config, error) {
if len(data) == 0 {
return Config{}, errors.New("empty data")
}
// ...
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `panic()` in exported functions
* `panic()` in package without `main`
* `panic()` for recoverable errors
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault converts panic to error return:
```go
func Process(input string) (Result, error) {
if input == "" {
return Result{}, errors.New("empty input")
}
// ...
}
```
Tip
Panic is acceptable for truly unrecoverable situations (programmer errors, invariant violations) but not for user input or external data.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.bare\_recover](/docs/reference/rules/go/bare-recover/)
* [rust.panic\_in\_library](/docs/reference/rules/rust/panic-in-library/)
# go.race_condition
> Detects potential race conditions from concurrent access to shared state.
Correctness High Common in Incidents
Detects potential race conditions from concurrent access to shared state without synchronization.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Race conditions cause some of the hardest bugs to find:
* **Intermittent failures** - May only happen under specific timing
* **Data corruption** - Concurrent writes produce garbage
* **Security vulnerabilities** - TOCTOU attacks exploit race windows
* **Impossible to reproduce** - Works in testing, fails in production
Go’s race detector catches some at runtime, but static analysis catches them earlier.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
var counter int
func incrementUnsafe() {
go func() {
counter++ // Race: concurrent read-modify-write
}()
}
```
Two goroutines incrementing simultaneously might both read the same value and write the same result.
```go
// ✅ After (mutex)
var (
counter int
mu sync.Mutex
)
func incrementSafe() {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
// ✅ After (atomic)
var counter int64
func incrementAtomic() {
go func() {
atomic.AddInt64(&counter, 1)
}()
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Shared variables accessed in goroutines without mutex
* Map access from multiple goroutines
* Struct field access without synchronization
* Closure capturing variables modified concurrently
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add mutex protection around shared variable access when the pattern allows safe transformation.
Note
Static race detection has inherent limitations. Unfault catches common patterns but recommends running `go test -race` for comprehensive coverage. Some advanced synchronization patterns may be flagged conservatively.
## Common Patterns
[Section titled “Common Patterns”](#common-patterns)
```go
// Atomic for simple counters
var count int64
atomic.AddInt64(&count, 1)
val := atomic.LoadInt64(&count)
// Mutex for complex state
type SafeCache struct {
mu sync.RWMutex
data map[string]string
}
func (c *SafeCache) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
func (c *SafeCache) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
// sync.Map for simple concurrent maps
var cache sync.Map
cache.Store("key", "value")
val, ok := cache.Load("key")
```
## Testing for Races
[Section titled “Testing for Races”](#testing-for-races)
```bash
# Run tests with race detector
go test -race ./...
# Build with race detector
go build -race
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.race\_condition](/docs/reference/rules/python/race-condition/)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
# go.rate_limiting
> Detects API endpoints without rate limiting protection.
Scalability Medium
Detects HTTP endpoints without rate limiting, which can be abused or overwhelmed.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without rate limiting:
* **DoS vulnerability** - Anyone can overwhelm your service
* **Resource exhaustion** - Uncontrolled traffic consumes all capacity
* **Unfair access** - One client can starve others
* **Cost explosion** - Cloud costs spike with traffic bursts
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no rate limiting)
func handler(w http.ResponseWriter, r *http.Request) {
// Any client can call this unlimited times
processRequest(w, r)
}
```
```go
// ✅ After (with rate limiting)
import "golang.org/x/time/rate"
var limiter = rate.NewLimiter(100, 10) // 100 req/sec, burst 10
func handler(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
processRequest(w, r)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* HTTP handlers without rate limit middleware
* Missing per-client rate limiting
* Expensive operations without throttling
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates rate limiting middleware:
```go
import "golang.org/x/time/rate"
// Per-client rate limiting
type ClientLimiter struct {
limiters map[string]*rate.Limiter
mu sync.RWMutex
rate rate.Limit
burst int
}
func (cl *ClientLimiter) GetLimiter(clientID string) *rate.Limiter {
cl.mu.RLock()
limiter, exists := cl.limiters[clientID]
cl.mu.RUnlock()
if exists {
return limiter
}
cl.mu.Lock()
limiter = rate.NewLimiter(cl.rate, cl.burst)
cl.limiters[clientID] = limiter
cl.mu.Unlock()
return limiter
}
```
Tip
Use Redis-based rate limiting for distributed systems where multiple instances share rate limit state.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_goroutines](/docs/reference/rules/go/unbounded-goroutines/)
* [typescript.rate\_limiting](/docs/reference/rules/typescript/rate-limiting/)
# go.redis.connection_pool
> Detects missing or misconfigured Redis connection pool settings.
Scalability High
Detects missing or misconfigured Redis connection pool settings.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Missing connection pool configuration:
* **Connection exhaustion** - Too many connections overwhelm Redis
* **Resource waste** - Idle connections consume memory
* **Timeouts** - No pool limits lead to starvation
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no pool configuration)
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
```
```go
// ✅ After (with connection pool settings)
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
PoolSize: 100,
MinIdleConns: 10,
PoolTimeout: 30 * time.Second,
MaxRetries: 3,
})
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Missing PoolSize configuration
* Missing MinIdleConns
* Missing PoolTimeout
* Unreasonable pool size values
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add appropriate connection pool configuration based on common best practices.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.redis.missing\_ttl](/docs/reference/rules/go/redis-missing-ttl/)
* [go.unbounded\_cache](/docs/reference/rules/go/unbounded-cache/)
# go.redis.missing_ttl
> Detects Redis SET operations without TTL.
Scalability High
Detects Redis SET operations without TTL.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Redis keys without TTL:
* **Memory growth** - Keys accumulate indefinitely
* **OOM risk** - Redis runs out of memory
* **Stale data** - Old values never expire
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no TTL)
err := client.Set(ctx, "user:123", userData, 0).Err()
```
```go
// ✅ After (with TTL)
err := client.Set(ctx, "user:123", userData, 24*time.Hour).Err()
```
```go
// ✅ Alternative (SetEX for explicit expiration)
err := client.SetEx(ctx, "session:abc", sessionData, 30*time.Minute).Err()
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Set with duration 0 (no expiry)
* Missing expiration on cache keys
* SetNX without subsequent Expire
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add TTL parameters to Redis SET operations.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.redis.connection\_pool](/docs/reference/rules/go/redis-connection-pool/)
* [go.unbounded\_cache](/docs/reference/rules/go/unbounded-cache/)
# go.reflect_in_hot_path
> Detects reflection usage in performance-critical code paths.
Performance Medium
Detects `reflect` package usage in frequently-called code paths.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Reflection in hot paths:
* **10-100x slower** - Reflection bypasses compile-time optimizations
* **More allocations** - Reflect creates temporary objects
* **No inlining** - Defeats compiler optimizations
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (reflection in loop)
func processItems(items []interface{}) {
for _, item := range items {
v := reflect.ValueOf(item)
// Reflection on every iteration
}
}
```
```go
// ✅ After (type assertion)
func processItems(items []Item) {
for _, item := range items {
// Direct field access, no reflection
process(item.Name)
}
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `reflect.ValueOf()` in loops
* `reflect.TypeOf()` in handlers
* Reflection in hot code paths
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault suggests type assertions or interfaces:
```go
// Use type switch instead of reflection
switch v := item.(type) {
case string:
processString(v)
case int:
processInt(v)
}
```
Tip
If you need reflection, cache type information outside loops. Use code generation for serialization (e.g., `easyjson`).
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.cpu\_in\_hot\_path](/docs/reference/rules/go/cpu-in-hot-path/)
* [go.regex\_compile](/docs/reference/rules/go/regex-compile/)
# go.regex_compile
> Detects regex patterns compiled inside loops or hot paths.
Performance Medium
Detects regex patterns compiled repeatedly inside loops or frequently-called functions.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Compiling regex in hot paths:
* **Wastes CPU** - Regex compilation is expensive
* **Increases latency** - Each request pays compilation cost
* **Scales poorly** - Impact grows with traffic
* **Triggers GC pressure** - Allocates memory repeatedly
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (compiled every call)
func validateEmail(email string) bool {
pattern := regexp.MustCompile(`^[\w.-]+@[\w.-]+\.\w+$`)
return pattern.MatchString(email)
}
```
```go
// ✅ After (compiled once)
var emailPattern = regexp.MustCompile(`^[\w.-]+@[\w.-]+\.\w+$`)
func validateEmail(email string) bool {
return emailPattern.MatchString(email)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `regexp.Compile()` inside functions
* `regexp.MustCompile()` inside loops
* Repeated pattern compilation in handlers
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault moves regex compilation to package level:
```go
// Moved to package level
var _pattern = regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
```
Tip
Use `regexp.MustCompile()` at package level - it panics on invalid patterns, catching errors at startup rather than runtime.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.regex\_compile](/docs/reference/rules/python/regex-compile/)
* [rust.regex\_compile](/docs/reference/rules/rust/regex-compile/)
# go.sentinel_error_comparison
> Detects direct error string comparison instead of errors.Is().
Correctness Medium
Detects error comparisons using `==` or string matching instead of `errors.Is()`.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Direct error comparison:
* **Misses wrapped errors** - `fmt.Errorf("%w", err)` won’t match
* **Breaks error chains** - Go 1.13+ error wrapping is ignored
* **Fragile** - String comparisons break on message changes
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (broken with wrapped errors)
if err == sql.ErrNoRows {
return nil
}
if err.Error() == "not found" {
return nil
}
```
```go
// ✅ After (handles wrapped errors)
if errors.Is(err, sql.ErrNoRows) {
return nil
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `err == sentinel` comparisons
* `err.Error() == "..."` string comparisons
* Missing `errors.Is()` usage
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault replaces direct comparison with `errors.Is()`:
```go
if errors.Is(err, sql.ErrNoRows) {
// ...
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
* [go.error\_type\_assertion](/docs/reference/rules/go/error-type-assertion/)
# go.slice_append_in_loop
> Detects inefficient slice append patterns in loops.
Performance Low
Detects slice appends in loops without pre-allocation, causing repeated reallocations.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Appending without pre-allocation:
* **Multiple reallocations** - Slice grows exponentially, copying each time
* **Memory churn** - Old backing arrays become garbage
* **GC pressure** - More work for garbage collector
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (grows slice repeatedly)
var result []Item
for _, id := range ids {
result = append(result, fetchItem(id))
}
```
```go
// ✅ After (pre-allocated)
result := make([]Item, 0, len(ids))
for _, id := range ids {
result = append(result, fetchItem(id))
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `append()` in loops without `make([]T, 0, n)` initialization
* Growing slices without capacity hint when size is known
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds slice pre-allocation:
```go
result := make([]Item, 0, len(ids))
```
Tip
If the exact size is unknown, estimate a reasonable upper bound. Over-allocating slightly is better than multiple reallocations.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.slice\_memory\_leak](/docs/reference/rules/go/slice-memory-leak/)
* [go.map\_without\_size\_hint](/docs/reference/rules/go/map-without-size-hint/)
# go.slice_memory_leak
> Detects slice operations that can cause memory leaks.
Performance Medium
Detects slice operations that retain references to underlying arrays, causing memory leaks.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Slice memory leaks cause:
* **Hidden memory retention** - Small slice keeps large array alive
* **Gradual memory growth** - Leaks accumulate over time
* **Hard to diagnose** - Memory profiler shows slices, not retained arrays
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (retains entire array)
func getPrefix(data []byte) []byte {
return data[:10] // Keeps entire backing array!
}
```
```go
// ✅ After (copies to new slice)
func getPrefix(data []byte) []byte {
prefix := make([]byte, 10)
copy(prefix, data[:10])
return prefix // Only 10 bytes retained
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Returning slice of large input
* Storing sub-slices in long-lived structures
* Appending without copying when source is discarded
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates copy patterns:
```go
result := make([]T, len(subslice))
copy(result, subslice)
```
Tip
Use `slices.Clone()` (Go 1.21+) for cleaner copy syntax: `result := slices.Clone(data[:10])`
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.slice\_append\_in\_loop](/docs/reference/rules/go/slice-append-in-loop/)
* [go.unbounded\_memory](/docs/reference/rules/go/unbounded-memory/)
# go.sql_injection
> Detects SQL queries built with string concatenation.
Correctness Critical Common in Incidents
Detects SQL queries built with string concatenation instead of parameterized queries.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
SQL injection is a critical security vulnerability:
* **Data theft** - Attackers can read your entire database
* **Data destruction** - DROP TABLE, DELETE, UPDATE at will
* **Authentication bypass** - Log in as any user
* **Privilege escalation** - Gain admin access
One unparameterized query can compromise your entire system.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
query := "SELECT * FROM users WHERE id = " + userID
db.Query(query)
// Also bad
query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", name)
```
If `userID` is `"1 OR 1=1"`, all users are returned. If `name` is `"'; DROP TABLE users; --"`, your data is gone.
```go
// ✅ After
db.Query("SELECT * FROM users WHERE id = ?", userID)
// Or with named parameters (sqlx)
db.NamedQuery("SELECT * FROM users WHERE name = :name", map[string]interface{}{"name": name})
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* String concatenation (`+`) in SQL query strings
* `fmt.Sprintf` with SQL keywords
* `strings.Replace` on query templates
* Variable interpolation in queries
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can convert concatenated queries to parameterized form when the query structure is unambiguous.
Note
Unfault avoids flagging known-safe patterns like constant queries and query builder methods. Dynamic table names may be flagged but require manual review.
## Database Driver Placeholders
[Section titled “Database Driver Placeholders”](#database-driver-placeholders)
```go
// PostgreSQL (lib/pq, pgx)
db.Query("SELECT * FROM users WHERE id = $1", id)
// MySQL
db.Query("SELECT * FROM users WHERE id = ?", id)
// SQLite
db.Query("SELECT * FROM users WHERE id = ?", id)
// Multiple parameters - PostgreSQL
db.Query("SELECT * FROM users WHERE id = $1 AND status = $2", id, status)
// Multiple parameters - MySQL
db.Query("SELECT * FROM users WHERE id = ? AND status = ?", id, status)
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.sql\_injection](/docs/reference/rules/python/sql-injection/)
* [rust.sql\_injection](/docs/reference/rules/rust/sql-injection/)
* [typescript.sql\_injection](/docs/reference/rules/typescript/sql-injection/)
# go.sync_dns_lookup
> Detects synchronous DNS lookups that can block goroutines.
Performance Medium
Detects synchronous DNS lookups that can block for seconds during resolution failures.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Synchronous DNS:
* **Blocks goroutines** - DNS can take seconds to timeout
* **Exhausts workers** - Blocked goroutines can’t serve requests
* **Cascades failures** - DNS issues affect all requests
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (synchronous)
func connect(host string) error {
addrs, err := net.LookupHost(host) // Can block for seconds
if err != nil {
return err
}
// ...
}
```
```go
// ✅ After (with timeout)
func connect(ctx context.Context, host string) error {
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, host) // Respects context timeout
if err != nil {
return err
}
// ...
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `net.LookupHost()` without context
* `net.LookupAddr()` without timeout
* DNS lookups in critical paths
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds context-aware DNS resolution:
```go
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, host)
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.http\_missing\_timeout](/docs/reference/rules/go/http-missing-timeout/)
* [python.sync\_dns\_lookup](/docs/reference/rules/python/sync-dns-lookup/)
# go.transaction_boundary
> Detects database operations without proper transaction boundaries.
Correctness High
Detects database operations spanning multiple queries without transaction boundaries.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Without transactions:
* **Partial updates** - Some changes commit, others fail
* **Data inconsistency** - Concurrent reads see intermediate states
* **Lost writes** - Overwrites happen between read and update
* **Recovery issues** - Can’t rollback partial operations
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (no transaction)
func transfer(db *sql.DB, from, to int, amount float64) error {
_, err := db.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
if err != nil {
return err
}
// If this fails, money is lost!
_, err = db.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
return err
}
```
```go
// ✅ After (with transaction)
func transfer(db *sql.DB, from, to int, amount float64) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, from)
if err != nil {
return err
}
_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, to)
if err != nil {
return err
}
return tx.Commit()
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Multiple `db.Exec()` calls without `db.Begin()`
* Missing `tx.Commit()` or `tx.Rollback()`
* Related writes without transaction wrapper
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault wraps operations in transactions:
```go
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// ... operations
return tx.Commit()
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.sql\_injection](/docs/reference/rules/go/sql-injection/)
* [python.transaction\_boundary](/docs/reference/rules/python/transaction-boundary/)
# go.type_assertion_no_ok
> Detects type assertions without the ok check.
Stability Medium
Detects type assertions without the `ok` check.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Type assertions without the ok check panic on failure:
* **Runtime panics** - Application crashes on unexpected types
* **No graceful handling** - No way to recover or provide fallback
* **Hidden assumptions** - Type expectations not visible in code
* **Testing blind spots** - Works until it receives unexpected data
A single unchecked type assertion can bring down your entire service.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
value := data.(string) // Panics if not string
```
If `data` is anything other than a string, this panics.
```go
// ✅ After
value, ok := data.(string)
if !ok {
return errors.New("expected string")
}
```
Now you handle the error gracefully.
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Type assertions without the second `ok` value
* Interface type assertions without checking
* Type switches with fallthrough to assertion
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault converts single-value assertions to two-value form with error handling.
## Common Patterns
[Section titled “Common Patterns”](#common-patterns)
```go
// Two-value assertion (safe)
str, ok := v.(string)
if !ok {
return fmt.Errorf("expected string, got %T", v)
}
// Type switch (safest for multiple types)
switch v := data.(type) {
case string:
return processString(v)
case int:
return processInt(v)
default:
return fmt.Errorf("unexpected type: %T", v)
}
// Assertion with default value
str, ok := v.(string)
if !ok {
str = "default"
}
```
## When Single-Value Is Safe
[Section titled “When Single-Value Is Safe”](#when-single-value-is-safe)
```go
// After a type check (still not recommended)
if _, ok := v.(string); ok {
str := v.(string) // Safe but redundant
}
// Better: use type switch
if str, ok := v.(string); ok {
// Use str directly
}
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
* [rust.unsafe\_unwrap](/docs/reference/rules/rust/unsafe-unwrap/)
# go.unbounded_cache
> Detects in-memory caches without size limits.
Stability Medium
Detects in-memory caches without size limits that can grow indefinitely.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unbounded caches cause:
* **Memory exhaustion** - Cache grows until OOM
* **GC pressure** - Large heaps slow garbage collection
* **Unpredictable scaling** - Memory grows with traffic
* **Silent degradation** - No errors until crash
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unbounded)
var cache = make(map[string]interface{})
func Get(key string) interface{} {
return cache[key]
}
func Set(key string, val interface{}) {
cache[key] = val // Grows forever!
}
```
```go
// ✅ After (bounded LRU)
import "github.com/hashicorp/golang-lru/v2"
var cache, _ = lru.New[string, interface{}](10000)
func Get(key string) (interface{}, bool) {
return cache.Get(key)
}
func Set(key string, val interface{}) {
cache.Add(key, val) // Evicts oldest when full
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Maps used as caches without eviction
* `sync.Map` without size limits
* Growing collections without bounds
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault suggests LRU cache libraries:
```go
import "github.com/hashicorp/golang-lru/v2"
// With TTL
import "github.com/hashicorp/golang-lru/v2/expirable"
cache := expirable.NewLRU[string, interface{}](
10000, // max entries
nil, // on evict callback
5 * time.Minute, // TTL
)
```
Tip
For distributed systems, consider Redis or Memcached instead of in-memory caches to share state across instances.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_memory](/docs/reference/rules/go/unbounded-memory/)
* [python.unbounded\_cache](/docs/reference/rules/python/unbounded-cache/)
# go.unbounded_goroutines
> Detects goroutine spawning without bounds on concurrency.
Scalability High Causes Production Outages
Detects `go func()` calls without bounds on concurrent goroutines.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unbounded goroutine spawning exhausts resources:
* **Memory exhaustion** - Each goroutine uses \~2KB+ stack
* **CPU thrashing** - Too many goroutines competing for CPU
* **Downstream collapse** - Thousands of requests hit your database simultaneously
* **OOM crash** - Eventually the process is killed
Input-controlled fan-out is especially dangerous-attackers can DoS your service.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
for _, item := range items {
go process(item) // Spawns len(items) goroutines
}
```
If `items` has 100,000 elements, you spawn 100,000 goroutines simultaneously.
```go
// ✅ After
sem := make(chan struct{}, 100) // Limit to 100 concurrent
for _, item := range items {
sem <- struct{}{} // Acquire
go func(item Item) {
defer func() { <-sem }() // Release
process(item)
}(item)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `go func()` inside loops without semaphore
* Goroutine spawning driven by external input
* Missing worker pool patterns
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can wrap loop-spawned goroutines with semaphore-based limiting when the transformation is straightforward.
Note
Unfault detects goroutines spawned in loops without visible bounds. If you’re using errgroup.SetLimit() or a worker pool pattern elsewhere, Unfault may still flag the loop for review.
## Best Practices
[Section titled “Best Practices”](#best-practices)
```go
// Worker pool pattern
func processItems(items []Item, workers int) {
ch := make(chan Item, len(items))
var wg sync.WaitGroup
// Fixed number of workers
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for item := range ch {
process(item)
}
}()
}
// Feed items to workers
for _, item := range items {
ch <- item
}
close(ch)
wg.Wait()
}
// errgroup with limit
import "golang.org/x/sync/errgroup"
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(100)
for _, item := range items {
item := item
g.Go(func() error {
return process(ctx, item)
})
}
return g.Wait()
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [python.unbounded\_concurrency](/docs/reference/rules/python/unbounded-concurrency/)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
# go.unbounded_memory
> Detects operations that can consume unbounded memory.
Stability High
Detects operations that can consume unbounded memory, leading to OOM crashes.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unbounded memory operations cause:
* **OOM crashes** - Process killed by kernel
* **Pod evictions** - Kubernetes kills memory-heavy pods
* **Performance degradation** - GC pressure increases
* **Cascading failures** - Memory pressure affects other services
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unbounded)
func readAll(r io.Reader) ([]byte, error) {
return io.ReadAll(r) // Could be gigabytes!
}
func collectAll(items <-chan Item) []Item {
var result []Item
for item := range items {
result = append(result, item) // Unbounded growth
}
return result
}
```
```go
// ✅ After (bounded)
func readLimited(r io.Reader, maxSize int64) ([]byte, error) {
return io.ReadAll(io.LimitReader(r, maxSize))
}
func collectBounded(items <-chan Item, maxItems int) []Item {
result := make([]Item, 0, min(maxItems, 1000))
for item := range items {
result = append(result, item)
if len(result) >= maxItems {
break
}
}
return result
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `io.ReadAll()` without size limits
* Unbounded slice appends in loops
* Growing maps without limits
* Collecting all results without pagination
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds size limits:
```go
// Limited reader
io.LimitReader(r, 10*1024*1024) // 10MB max
```
Tip
Set memory limits in Kubernetes with `resources.limits.memory` to prevent unbounded growth from affecting other pods.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unbounded\_cache](/docs/reference/rules/go/unbounded-cache/)
* [go.large\_response\_memory](/docs/reference/rules/go/large-response-memory/)
* [python.unbounded\_memory](/docs/reference/rules/python/unbounded-memory/)
# go.unbounded_retry
> Detects retry loops without proper bounds or backoff.
Stability High
Detects retry patterns that don’t have proper bounds, which can cause infinite loops on permanent failures.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unbounded retries cause:
* **Infinite loops** - Permanent failures never stop retrying
* **Resource exhaustion** - CPU and connections consumed by retries
* **DoS on dependencies** - Overwhelming already-struggling services
* **Extended outages** - Retries prevent recovery
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unbounded)
func fetchData() error {
for {
if err := callAPI(); err == nil {
return nil
}
time.Sleep(time.Second)
}
}
```
```go
// ✅ After (bounded with backoff)
func fetchData() error {
maxRetries := 5
backoff := time.Second
for i := 0; i < maxRetries; i++ {
if err := callAPI(); err == nil {
return nil
}
time.Sleep(backoff)
backoff *= 2 // Exponential backoff
if backoff > time.Minute {
backoff = time.Minute
}
}
return errors.New("max retries exceeded")
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `for {}` loops with retry patterns
* Missing max retry count
* Missing backoff delays
* Retries without jitter
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault generates patches using retry libraries:
```go
import "github.com/cenkalti/backoff/v4"
func fetchData() error {
operation := func() error {
return callAPI()
}
b := backoff.NewExponentialBackOff()
b.MaxElapsedTime = 2 * time.Minute
b.MaxInterval = 30 * time.Second
return backoff.Retry(operation, b)
}
```
Tip
Add jitter to backoff to prevent thundering herd when multiple clients retry simultaneously.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.circuit\_breaker](/docs/reference/rules/go/circuit-breaker/)
* [go.http\_retry](/docs/reference/rules/go/http-retry/)
* [python.unbounded\_retry](/docs/reference/rules/python/unbounded-retry/)
# go.uncancelled_context
> Detects context.WithCancel/WithTimeout without cancellation.
Stability Medium
Detects `context.WithCancel()` and `context.WithTimeout()` without calling the cancel function, leaking resources.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Uncancelled contexts cause:
* **Memory leaks** - Context goroutines never terminate
* **Resource exhaustion** - Over time, leaks accumulate
* **Timer leaks** - WithTimeout/WithDeadline leak timers
* **Goroutine leaks** - Background goroutines never stop
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (cancel never called)
func process() {
ctx, _ := context.WithTimeout(context.Background(), time.Second)
doWork(ctx)
// Timer goroutine leaked!
}
```
```go
// ✅ After (cancel called)
func process() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() // Always call cancel!
doWork(ctx)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* `context.WithCancel()` with unused cancel func
* `context.WithTimeout()` without defer cancel
* `context.WithDeadline()` without cleanup
* Ignored cancel functions (`_`)
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds `defer cancel()`:
```go
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
```
Tip
Always call cancel even if the operation completes successfully. It releases resources immediately rather than waiting for timeout.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.context\_background](/docs/reference/rules/go/context-background/)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
# go.unchecked_error
> Detects Go code that ignores error return values.
Correctness Medium
Detects Go code that ignores error return values.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Go’s error handling philosophy requires explicit handling:
* **Silent failures** - Operations fail but code continues as if successful
* **Data corruption** - Writes fail, reads return garbage, code proceeds
* **Debugging nightmare** - Errors surface far from their origin
* **Unexpected panics** - Nil results used without checking
Go makes errors explicit for a reason. Ignoring them defeats the language’s safety design.
## Example
[Section titled “Example”](#example)
```go
// ❌ Before
os.ReadFile("config.json") // Error ignored
json.Unmarshal(data, &config) // Error ignored
```
If the file doesn’t exist or JSON is invalid, you’ll get mysterious failures later.
```go
// ✅ After
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("reading config: %w", err)
}
if err := json.Unmarshal(data, &config); err != nil {
return fmt.Errorf("parsing config: %w", err)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Function calls with error return values that are discarded
* `_` used to explicitly ignore errors
* Error variables declared but never checked
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault adds error handling with appropriate error wrapping.
## Common Patterns
[Section titled “Common Patterns”](#common-patterns)
```go
// Return early on error
data, err := fetch()
if err != nil {
return nil, err
}
// Wrap with context
if err := save(data); err != nil {
return fmt.Errorf("saving data: %w", err)
}
// Log and continue (when recovery is possible)
if err := sendMetric(m); err != nil {
log.Warn().Err(err).Msg("failed to send metric")
// Continue - metrics are not critical
}
// Explicit ignore (rare, document why)
_ = conn.Close() // Best effort, already handling error
```
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [rust.ignored\_result](/docs/reference/rules/rust/ignored-result/)
* [typescript.empty\_catch](/docs/reference/rules/typescript/empty-catch/)
# go.unhandled_error_goroutine
> Detects errors not handled in goroutines.
Stability High
Detects errors not handled in goroutines.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unhandled errors in goroutines:
* **Silent failures** - Errors are lost without logging
* **Debugging difficulty** - No trace of what went wrong
* **Inconsistent state** - Failed operations go unnoticed
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (error silently ignored)
go func() {
result, err := processData(data)
if err != nil {
return // Error is lost!
}
// use result
}()
```
```go
// ✅ After (error properly handled)
go func() {
result, err := processData(data)
if err != nil {
log.Printf("processData failed: %v", err)
errorChan <- err
return
}
resultChan <- result
}()
```
```go
// ✅ Alternative (with error group)
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return processData(ctx, data)
})
if err := g.Wait(); err != nil {
log.Printf("goroutine failed: %v", err)
}
```
## What Unfault Detects
[Section titled “What Unfault Detects”](#what-unfault-detects)
* Error returns ignored in goroutines
* Missing error logging in goroutines
* Goroutines swallowing errors silently
## Auto-Fix
[Section titled “Auto-Fix”](#auto-fix)
Unfault can add error logging or channel-based error propagation patterns.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* [go.unchecked\_error](/docs/reference/rules/go/unchecked-error/)
* [go.goroutine\_leak](/docs/reference/rules/go/goroutine-leak/)
# go.unsafe_template
> Detects unsafe HTML template usage that can lead to XSS vulnerabilities.
Security Critical
Detects unsafe HTML template usage that can lead to cross-site scripting (XSS) vulnerabilities.
## Why It Matters
[Section titled “Why It Matters”](#why-it-matters)
Unsafe templates enable:
* **XSS attacks** - Attackers inject malicious scripts
* **Session hijacking** - Stolen cookies and tokens
* **Data theft** - Access to sensitive page content
* **Malware distribution** - Redirects to malicious sites
## Example
[Section titled “Example”](#example)
```go
// ❌ Before (unsafe - uses text/template)
import "text/template"
func handler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
t := template.New("page")
t.Parse(`Hello {{.}}
`)
t.Execute(w, name) // XSS if name contains