
Focused Fix
- 59 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Focused Fix is a Claude skill that scopes a bugfix to the smallest possible change set using a change-scope analyzer, preventing refactors and scope creep.
About
Focused Fix is a Claude skill that enforces a minimal-change approach to bug fixing. It uses a change_scope_analyzer.py script to identify the smallest set of files that resolve an issue, then validates that the actual diff stays within that scope. A developer uses it when triaging a bug, scoping a hotfix, or keeping a bugfix PR from creeping into refactors.
- Enforces minimal-change bug fixing to prevent scope creep
- change_scope_analyzer.py identifies the smallest set of files to touch
- Anti-pattern table catches 'while I'm here' refactors in bugfix PRs
Focused Fix by the numbers
- 59 all-time installs (skills.sh)
- Ranked #294 of 596 Debugging skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
focused-fix capabilities & compatibility
Free; runs a local Python script, no API keys.
- Capabilities
- code review · focused fix
- Use cases
- debugging · code review
- Pricing
- Free
What focused-fix says it does
The **Focused Fix** skill enforces a disciplined minimal-change approach to bug fixing.
python scripts/change_scope_analyzer.py --bug "Login fails when email has + character" --path ./src
Make ONLY the changes needed to fix the bug
npx skills add https://github.com/borghei/claude-skills --skill focused-fixAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 59 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Scope a bugfix to the minimal set of files and keep the diff small enough for low-risk review.
Who is it for?
Triaging a bug into the minimal files to touch, scoping a release-blocking hotfix, or stopping a bugfix PR from bloating into refactors.
Skip if: Large refactors, feature work, or dependency upgrades bundled with the fix.
When should I use this skill?
The user asks to fix a bug with minimal changes, scope a minimal repair, or find the minimal set of files to change.
What you get
A minimal, reviewable diff limited to the files that actually fix the bug, plus a regression test.
- Minimal file-change scope report from change_scope_analyzer.py
- Focused PR with a regression test
By the numbers
- 1 bundled script (change_scope_analyzer.py)
- 6-step focused bugfix workflow with per-step validation
Files
Focused Fix
Category: Engineering
Domain: Debugging & Maintenance
Overview
The Focused Fix skill enforces a disciplined minimal-change approach to bug fixing. Instead of refactoring or improving code during a bugfix, it identifies the smallest possible change set that resolves the issue. This reduces risk, simplifies code review, and prevents scope creep.
Use when
- The user asks to "fix a bug with minimal changes", "do a focused bugfix", or "scope a minimal repair"
- A bug report needs triage to identify the smallest set of files to touch
- A PR is at risk of scope creep (unrelated refactors, style changes, "nearby" fixes)
- A hotfix or release-blocker needs a low-risk, reviewable change set
- The user asks "what is the minimal change to fix X?" or "which files do I need to touch for this bug?"
Quick Start
# Analyze a bug description to identify minimal change scope
python scripts/change_scope_analyzer.py --bug "Login fails when email has + character" --path ./src
# Analyze with JSON output
python scripts/change_scope_analyzer.py --bug "API returns 500 on empty array input" --path ./src --format json
# Analyze with specific file extensions
python scripts/change_scope_analyzer.py --bug "CSS overflow on mobile" --path ./src --extensions .css .scss .htmlTools Overview
| Tool | Purpose | Key Flags |
|---|---|---|
change_scope_analyzer.py | Identify minimal files to change for a bugfix | --bug, --path, --extensions, --format |
change_scope_analyzer.py
Analyzes a bug description against a codebase to identify:
- Files most likely related to the bug (keyword matching, import tracing)
- Estimated change scope (number of files, lines)
- Risk assessment for the change
- Recommended fix approach (minimal vs. structural)
Workflows
Focused Bugfix Workflow
1. Write a clear bug description — reproduction steps, expected vs actual behavior
- Validate: the description names the observable failure, not a guess at the cause
2. Run `change_scope_analyzer.py` to identify scope
- Validate: analyzer output lists concrete files and an estimated line count
3. Review the recommended files and approach
- Validate: recommended approach is "minimal" — if it says "structural", stop and scope a refactor PR separately
4. Make ONLY the changes needed to fix the bug
- Validate:
git diff --statmatches (or is smaller than) the analyzer's recommendation
5. Verify no unrelated changes leaked in
- Validate:
git diffshows no formatting-only changes, no unrelated imports, no "while I'm here" edits
6. Submit PR with focused change set
- Validate: commit message states the exact bug fixed, and a regression test is included
Scope Validation
1. After making changes, re-run analyzer 2. Compare actual changes against recommended scope 3. Flag any out-of-scope modifications for separate PRs
- Validate: any file touched that was not in the analyzer recommendation has a one-line justification or is reverted
Reference Documentation
- Focused Fix Methodology - Principles, anti-patterns, and decision framework
Common Patterns
Do
- Fix the exact bug reported
- Add a regression test for the fix
- Document why the fix works in the commit message
- Keep the diff as small as possible
Don't
- Refactor surrounding code during a bugfix
- Fix "nearby" issues in the same PR
- Change formatting or style in touched files
- Add features disguised as bugfixes
Anti-patterns
| Anti-pattern | Failure mode | Fix |
|---|---|---|
| "While I'm here" refactors in the bugfix PR | Blast radius explodes; review time multiplies; unrelated regressions masked by the real fix | Open a separate PR for the refactor, tagged as refactor: not fix: |
| Reformatting or auto-save style changes in touched files | Diff becomes unreadable; real fix hidden in 200 lines of whitespace | Revert style changes before committing; configure the editor to format-on-save only for new files |
| Fixing the symptom in the wrong layer | Bug returns in a new form; accumulates workaround debt | Trace to the root layer — analyzer's keyword-match output is a hint, not an answer |
| Skipping the regression test "because the fix is obvious" | Bug silently returns on a refactor 6 months later | Every fix: commit adds at least one failing-then-passing test |
Treating change_scope_analyzer.py output as authoritative | Analyzer is keyword/import-based, not semantic — misses dynamic dispatch, reflection, config-driven paths | Use it as a starting set; grep for callers and tests before committing to the scope |
| Bundling the fix with a dependency upgrade | Two risk profiles in one PR; if rollback is needed, both are lost | Land the upgrade separately, then the fix against the upgraded baseline |
Focused Fix Methodology
Core Principle
A focused fix changes the minimum number of lines needed to resolve a specific bug. Nothing more, nothing less.
Why Minimal Changes Matter
1. Reduced Risk: Every line changed is a line that could introduce a new bug 2. Faster Review: Small diffs are reviewed more thoroughly 3. Clean History: Git blame stays meaningful when fixes are surgical 4. Easier Rollback: Small changes are trivially reverted if problems emerge 5. Clear Accountability: Each change has a clear, traceable purpose
The Focused Fix Decision Framework
Step 1: Reproduce
- Confirm the bug exists with a concrete reproduction case
- Document exact steps, inputs, and expected vs. actual behavior
- Identify the environment and conditions
Step 2: Locate
- Trace the execution path from symptom to root cause
- Use the bug description to identify keyword-relevant files
- Follow import chains and call graphs
- Focus on the layer where the bug manifests
Step 3: Scope
- Identify the minimum set of files that need changes
- Estimate lines of change per file
- Classify: is this a logic error, data error, or configuration error?
Step 4: Fix
- Change ONLY what is necessary to fix the bug
- Do not refactor, reformat, or "improve" touched code
- If you notice other issues, create separate tickets
Step 5: Verify
- Confirm the fix resolves the original reproduction case
- Run existing tests to ensure no regressions
- Add a regression test for this specific bug
Step 6: Review
- Compare actual changes against initial scope estimate
- Remove any accidental scope creep
- Ensure commit message explains the "why"
Scope Classification
| Category | Files Changed | Lines Changed | Risk |
|---|---|---|---|
| Micro Fix | 1 file | 1-5 lines | Very Low |
| Small Fix | 1-2 files | 5-20 lines | Low |
| Medium Fix | 2-4 files | 20-50 lines | Medium |
| Large Fix | 5+ files | 50+ lines | High - consider splitting |
Anti-Patterns
The "While I'm Here" Anti-Pattern
Fixing unrelated issues because you happen to be in the file. Create separate tickets instead.
The Refactor Disguised as a Bugfix
Restructuring code to fix a bug when a simpler change would work. Save refactoring for dedicated PRs.
The Test-Only Fix
Adding tests that pass without changing production code. The bug is still there.
The Configuration Shotgun
Changing multiple config values hoping one fixes the issue. Identify the specific root cause.
Keywords to Scope Mapping
Common bug description keywords map to likely code areas:
- "login", "auth", "password" -> authentication modules
- "crash", "exception", "error" -> error handling, try/catch blocks
- "slow", "timeout", "performance" -> database queries, API calls, loops
- "display", "render", "layout" -> frontend components, CSS, templates
- "data", "missing", "null" -> data validation, null checks, defaults
- "permission", "access", "denied" -> authorization, role checks
- "email", "notification", "send" -> messaging, notification services
#!/usr/bin/env python3
"""
Change Scope Analyzer - Identify minimal files to change for a bugfix.
Analyzes a bug description against a codebase to find the most relevant files,
estimate change scope, and recommend a focused fix approach.
Author: Claude Skills Engineering Team
License: MIT
"""
import argparse
import json
import os
import re
import sys
from collections import Counter, defaultdict
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import List, Dict, Optional, Set, Tuple
import math
@dataclass
class FileRelevance:
"""A file's relevance to the bug description."""
file_path: str
relevance_score: float
matching_keywords: List[str]
matching_lines: List[Tuple[int, str]] # (line_num, line_text)
estimated_change_lines: int
confidence: str # high, medium, low
reason: str
@dataclass
class ScopeAnalysis:
"""Complete scope analysis result."""
bug_description: str
extracted_keywords: List[str]
total_files_scanned: int
relevant_files: List[FileRelevance] = field(default_factory=list)
scope_category: str = "" # micro, small, medium, large
estimated_total_files: int = 0
estimated_total_lines: int = 0
risk_level: str = ""
recommended_approach: str = ""
warnings: List[str] = field(default_factory=list)
# Keyword categories that map bug descriptions to code areas
KEYWORD_DOMAINS = {
"authentication": ["login", "auth", "password", "credential", "session", "token", "jwt",
"oauth", "sso", "signin", "signup", "logout", "2fa", "mfa"],
"authorization": ["permission", "access", "denied", "forbidden", "role", "acl", "rbac",
"policy", "scope", "privilege"],
"api": ["api", "endpoint", "request", "response", "rest", "graphql", "route", "handler",
"middleware", "controller", "status code", "http", "404", "500", "401", "403"],
"database": ["query", "database", "sql", "table", "column", "migration", "orm",
"transaction", "index", "foreign key", "constraint", "join"],
"frontend": ["display", "render", "layout", "css", "style", "component", "template",
"ui", "ux", "button", "form", "input", "modal", "responsive", "mobile"],
"data_validation": ["validation", "null", "undefined", "empty", "missing", "format",
"parse", "convert", "type", "schema", "regex", "pattern"],
"performance": ["slow", "timeout", "performance", "latency", "cache", "memory", "leak",
"cpu", "load", "optimize", "n+1", "batch"],
"error_handling": ["crash", "exception", "error", "bug", "fail", "broken", "traceback",
"stack trace", "panic", "unhandled"],
"email": ["email", "mail", "notification", "send", "smtp", "template", "newsletter"],
"file_handling": ["file", "upload", "download", "path", "directory", "permission",
"read", "write", "stream", "encoding", "utf"],
"integration": ["webhook", "callback", "integration", "third-party", "external",
"service", "sdk", "client"],
"configuration": ["config", "environment", "env", "setting", "flag", "feature",
"toggle", "variable"],
}
# File path patterns that correlate with keyword domains
PATH_DOMAIN_PATTERNS = {
"authentication": [r'auth', r'login', r'session', r'identity'],
"authorization": [r'perm', r'role', r'policy', r'guard'],
"api": [r'api', r'route', r'controller', r'handler', r'endpoint', r'middleware'],
"database": [r'model', r'schema', r'migration', r'repository', r'dao', r'query'],
"frontend": [r'component', r'view', r'template', r'page', r'layout', r'style'],
"data_validation": [r'valid', r'schema', r'form', r'sanitiz', r'filter'],
"performance": [r'cache', r'queue', r'worker', r'task', r'job'],
"error_handling": [r'error', r'exception', r'handler', r'middleware'],
"email": [r'mail', r'email', r'notification', r'message'],
"file_handling": [r'upload', r'storage', r'file', r'media', r'asset'],
"integration": [r'client', r'service', r'integration', r'webhook', r'external'],
"configuration": [r'config', r'setting', r'env'],
}
SCAN_EXTENSIONS = {
".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go", ".rs", ".rb",
".php", ".cs", ".swift", ".kt", ".scala", ".css", ".scss", ".html",
".vue", ".svelte", ".sql", ".yaml", ".yml", ".toml", ".json", ".xml",
}
SKIP_DIRS = {
".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build",
".tox", ".mypy_cache", ".eggs", "vendor", ".next", ".nuxt",
}
def extract_keywords(bug_description: str) -> Tuple[List[str], Dict[str, float]]:
"""Extract keywords from bug description and score by domain relevance."""
words = re.findall(r'\b[a-zA-Z_]{2,}\b', bug_description.lower())
# Remove common stop words
stop_words = {"the", "is", "at", "in", "on", "to", "of", "and", "or", "for",
"it", "an", "as", "by", "be", "was", "are", "been", "has", "have",
"had", "do", "does", "did", "will", "would", "could", "should",
"when", "where", "what", "how", "why", "with", "from", "not", "but",
"this", "that", "they", "them", "their", "there", "than", "then",
"also", "just", "only", "very", "can", "into", "some", "other"}
keywords = [w for w in words if w not in stop_words]
# Score domains
domain_scores: Dict[str, float] = defaultdict(float)
for word in keywords:
for domain, domain_keywords in KEYWORD_DOMAINS.items():
for dk in domain_keywords:
if dk in word or word in dk:
domain_scores[domain] += 1.0
elif _fuzzy_match(word, dk):
domain_scores[domain] += 0.5
return keywords, dict(domain_scores)
def _fuzzy_match(a: str, b: str) -> bool:
"""Simple fuzzy matching - checks if strings share significant overlap."""
if len(a) < 3 or len(b) < 3:
return False
shorter = min(a, b, key=len)
longer = max(a, b, key=len)
return shorter in longer
def collect_files(path: Path, extensions: Optional[Set[str]] = None) -> List[Path]:
"""Collect source files from the codebase."""
exts = extensions or SCAN_EXTENSIONS
files = []
if path.is_file():
if path.suffix in exts:
files.append(path)
else:
for root, dirs, filenames in os.walk(path):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for fname in filenames:
fp = Path(root) / fname
if fp.suffix in exts:
files.append(fp)
return files
def score_file(file_path: Path, keywords: List[str], domain_scores: Dict[str, float],
base_path: Path) -> Optional[FileRelevance]:
"""Score a file's relevance to the bug."""
score = 0.0
matching_keywords = []
matching_lines = []
reasons = []
rel_path = str(file_path.relative_to(base_path)).lower()
# Score based on path matching domain patterns
for domain, patterns in PATH_DOMAIN_PATTERNS.items():
domain_weight = domain_scores.get(domain, 0)
if domain_weight > 0:
for pat in patterns:
if re.search(pat, rel_path, re.IGNORECASE):
score += domain_weight * 2
reasons.append(f"Path matches {domain} domain")
break
# Score based on file content keyword matching
try:
content = file_path.read_text(encoding="utf-8", errors="ignore")
except (OSError, PermissionError):
return None
lines = content.split("\n")
content_lower = content.lower()
for keyword in keywords:
if keyword in content_lower:
matching_keywords.append(keyword)
# Find specific matching lines (up to 3 per keyword)
count = 0
for i, line in enumerate(lines, 1):
if keyword in line.lower() and count < 3:
matching_lines.append((i, line.strip()[:100]))
count += 1
score += 1.0
# Bonus for filename matching keywords
fname = file_path.stem.lower()
for keyword in keywords:
if keyword in fname:
score += 3.0
reasons.append(f"Filename contains '{keyword}'")
if score < 0.5:
return None
# Estimate change scope based on matching density
match_density = len(matching_keywords) / max(len(keywords), 1)
estimated_lines = max(1, int(match_density * 10))
# Determine confidence
if score >= 8:
confidence = "high"
elif score >= 4:
confidence = "medium"
else:
confidence = "low"
reason = "; ".join(reasons) if reasons else f"Content matches: {', '.join(matching_keywords[:5])}"
return FileRelevance(
file_path=str(file_path.relative_to(base_path)),
relevance_score=round(score, 2),
matching_keywords=matching_keywords,
matching_lines=matching_lines[:5],
estimated_change_lines=estimated_lines,
confidence=confidence,
reason=reason,
)
def analyze_scope(bug_description: str, path: Path,
extensions: Optional[Set[str]] = None) -> ScopeAnalysis:
"""Perform full scope analysis."""
keywords, domain_scores = extract_keywords(bug_description)
files = collect_files(path, extensions)
analysis = ScopeAnalysis(
bug_description=bug_description,
extracted_keywords=keywords,
total_files_scanned=len(files),
)
# Score all files
scored = []
for fp in files:
relevance = score_file(fp, keywords, domain_scores, path)
if relevance:
scored.append(relevance)
# Sort by relevance score descending
scored.sort(key=lambda x: x.relevance_score, reverse=True)
# Keep top results (max 15)
analysis.relevant_files = scored[:15]
analysis.estimated_total_files = min(len(scored), 15)
analysis.estimated_total_lines = sum(f.estimated_change_lines for f in analysis.relevant_files[:5])
# Classify scope
high_confidence = [f for f in analysis.relevant_files if f.confidence == "high"]
if len(high_confidence) <= 1 and analysis.estimated_total_lines <= 5:
analysis.scope_category = "micro"
analysis.risk_level = "very low"
elif len(high_confidence) <= 2 and analysis.estimated_total_lines <= 20:
analysis.scope_category = "small"
analysis.risk_level = "low"
elif len(high_confidence) <= 4 and analysis.estimated_total_lines <= 50:
analysis.scope_category = "medium"
analysis.risk_level = "medium"
else:
analysis.scope_category = "large"
analysis.risk_level = "high"
analysis.warnings.append("Large scope detected. Consider breaking into smaller fixes.")
# Generate approach recommendation
if analysis.scope_category in ("micro", "small"):
analysis.recommended_approach = (
"Direct fix: Change the identified file(s) with minimal modifications. "
"Add a regression test targeting the specific bug scenario."
)
elif analysis.scope_category == "medium":
analysis.recommended_approach = (
"Targeted fix: Focus on the top 2-3 high-confidence files first. "
"Verify the fix resolves the issue before touching additional files. "
"Consider if all changes are truly necessary for this fix."
)
else:
analysis.recommended_approach = (
"Structural fix needed: This bug may require changes across multiple layers. "
"Consider splitting into multiple PRs. Start with the core fix, "
"then address cascading changes in follow-up PRs."
)
if not analysis.relevant_files:
analysis.warnings.append("No relevant files found. Try refining the bug description with more specific terms.")
return analysis
def format_human(analysis: ScopeAnalysis) -> str:
"""Format results for human reading."""
lines = []
lines.append("=" * 65)
lines.append("CHANGE SCOPE ANALYSIS")
lines.append("=" * 65)
lines.append(f"Bug: {analysis.bug_description}")
lines.append(f"Keywords: {', '.join(analysis.extracted_keywords[:10])}")
lines.append(f"Files scanned: {analysis.total_files_scanned}")
lines.append("")
lines.append(f"Scope: {analysis.scope_category.upper()}")
lines.append(f"Risk: {analysis.risk_level}")
lines.append(f"Estimated files to change: {analysis.estimated_total_files}")
lines.append(f"Estimated lines to change: {analysis.estimated_total_lines}")
lines.append("")
lines.append(f"Recommended approach:")
lines.append(f" {analysis.recommended_approach}")
lines.append("")
if analysis.warnings:
lines.append("Warnings:")
for w in analysis.warnings:
lines.append(f" ! {w}")
lines.append("")
if analysis.relevant_files:
lines.append("Relevant Files (by relevance):")
lines.append("-" * 55)
for i, f in enumerate(analysis.relevant_files, 1):
lines.append(f" {i}. [{f.confidence.upper():6s}] {f.file_path} (score: {f.relevance_score})")
lines.append(f" Keywords: {', '.join(f.matching_keywords[:5])}")
lines.append(f" Est. changes: ~{f.estimated_change_lines} lines")
lines.append(f" Reason: {f.reason}")
if f.matching_lines:
for ln, text in f.matching_lines[:2]:
lines.append(f" L{ln}: {text}")
lines.append("")
lines.append("=" * 65)
return "\n".join(lines)
def format_json(analysis: ScopeAnalysis) -> str:
"""Format results as JSON."""
data = {
"bug_description": analysis.bug_description,
"extracted_keywords": analysis.extracted_keywords,
"total_files_scanned": analysis.total_files_scanned,
"scope_category": analysis.scope_category,
"risk_level": analysis.risk_level,
"estimated_total_files": analysis.estimated_total_files,
"estimated_total_lines": analysis.estimated_total_lines,
"recommended_approach": analysis.recommended_approach,
"warnings": analysis.warnings,
"relevant_files": [
{
"file_path": f.file_path,
"relevance_score": f.relevance_score,
"matching_keywords": f.matching_keywords,
"estimated_change_lines": f.estimated_change_lines,
"confidence": f.confidence,
"reason": f.reason,
}
for f in analysis.relevant_files
],
}
return json.dumps(data, indent=2)
def main():
parser = argparse.ArgumentParser(
description="Change Scope Analyzer - Identify minimal files to change for a bugfix"
)
parser.add_argument("--bug", required=True, help="Bug description text")
parser.add_argument("--path", required=True, help="Path to codebase to analyze")
parser.add_argument("--extensions", nargs="+",
help="File extensions to scan (e.g., .py .js .ts)")
parser.add_argument("--format", choices=["human", "json"], default="human",
help="Output format (default: human)")
args = parser.parse_args()
path = Path(args.path)
if not path.exists():
print(f"Error: Path not found: {args.path}", file=sys.stderr)
sys.exit(1)
extensions = None
if args.extensions:
extensions = {ext if ext.startswith(".") else f".{ext}" for ext in args.extensions}
analysis = analyze_scope(args.bug, path, extensions)
if args.format == "json":
print(format_json(analysis))
else:
print(format_human(analysis))
if __name__ == "__main__":
main()
Related skills
FAQ
What does change_scope_analyzer.py output?
It lists files most likely related to the bug via keyword matching and import tracing, an estimated change scope, a risk assessment, and a recommended fix approach (minimal vs structural).
Should I trust the analyzer's file list as authoritative?
No; it is keyword and import based, not semantic, so it misses dynamic dispatch and config-driven paths. Use it as a starting set and grep for callers and tests first.