33import ast
44import json
55import re
6+ from pathlib import Path
67
78import yaml
89
@@ -43,49 +44,149 @@ def attribute(self) -> Attribute:
4344 def assess (self , repository : Repository ) -> Finding :
4445 """Check for CLAUDE.md file in repository root.
4546
46- Pass criteria: CLAUDE.md exists
47- Scoring: Binary (100 if exists, 0 if not)
47+ Pass criteria:
48+ - CLAUDE.md exists with >50 bytes, OR
49+ - CLAUDE.md is a symlink to a file with >50 bytes, OR
50+ - CLAUDE.md contains @ reference to a file with >50 bytes, OR
51+ - AGENTS.md exists with >50 bytes (alternative)
52+
53+ Security:
54+ - @ references are restricted to relative paths within repository
55+ - Path traversal attempts (../) and absolute paths are rejected
56+
57+ Scoring:
58+ - 100 if CLAUDE.md passes (direct, symlink, or @ reference)
59+ - 90 if AGENTS.md exists without CLAUDE.md
60+ - 25 if CLAUDE.md exists but is minimal without valid references
61+ - 0 if neither file exists
4862 """
4963 claude_md_path = repository .path / "CLAUDE.md"
64+ agents_md_path = repository .path / "AGENTS.md"
5065
51- # Fix TOCTOU: Use try-except around file read instead of existence check
66+ # Check for CLAUDE.md first
5267 try :
53- with open (claude_md_path , "r" , encoding = "utf-8" ) as f :
68+ # Resolve symlinks if CLAUDE.md is a symlink
69+ resolved_path = claude_md_path .resolve (strict = True )
70+ is_symlink = claude_md_path .is_symlink ()
71+
72+ with open (resolved_path , "r" , encoding = "utf-8" ) as f :
5473 content = f .read ()
5574
5675 size = len (content )
57- if size < 50 :
58- # File exists but is too small
76+
77+ # Check if file has sufficient content
78+ if size >= 50 :
79+ evidence = [f"CLAUDE.md found at { claude_md_path } " ]
80+ if is_symlink :
81+ target = (
82+ resolved_path .relative_to (repository .path )
83+ if resolved_path .is_relative_to (repository .path )
84+ else resolved_path
85+ )
86+ evidence .append (f"Symlink to { target } ({ size } bytes)" )
87+
88+ # Bonus: Check if AGENTS.md also exists
89+ if self ._check_agents_md_exists (agents_md_path ):
90+ evidence .append ("AGENTS.md also present (cross-tool compatibility)" )
91+
5992 return Finding (
6093 attribute = self .attribute ,
61- status = "fail " ,
62- score = 25 .0 ,
63- measured_value = f" { size } bytes " ,
64- threshold = ">50 bytes " ,
65- evidence = [ f"CLAUDE.md exists but is minimal ( { size } bytes)" ] ,
66- remediation = self . _create_remediation () ,
94+ status = "pass " ,
95+ score = 100 .0 ,
96+ measured_value = "present " ,
97+ threshold = "present " ,
98+ evidence = evidence ,
99+ remediation = None ,
67100 error_message = None ,
68101 )
69102
103+ # File is small - check for @ references
104+ referenced_file = self ._extract_at_reference (content )
105+ if referenced_file :
106+ ref_path = repository .path / referenced_file
107+ ref_content , ref_size = self ._read_referenced_file (ref_path )
108+
109+ if ref_content and ref_size >= 50 :
110+ evidence = [
111+ f"CLAUDE.md found with @ reference to { referenced_file } " ,
112+ f"Referenced file contains { ref_size } bytes" ,
113+ ]
114+
115+ # Bonus: Check if AGENTS.md also exists
116+ if self ._check_agents_md_exists (agents_md_path ):
117+ evidence .append (
118+ "AGENTS.md also present (cross-tool compatibility)"
119+ )
120+
121+ return Finding (
122+ attribute = self .attribute ,
123+ status = "pass" ,
124+ score = 100.0 ,
125+ measured_value = f"@ reference to { referenced_file } " ,
126+ threshold = "present" ,
127+ evidence = evidence ,
128+ remediation = None ,
129+ error_message = None ,
130+ )
131+ else :
132+ # Referenced file doesn't exist or is too small
133+ return Finding (
134+ attribute = self .attribute ,
135+ status = "fail" ,
136+ score = 25.0 ,
137+ measured_value = f"{ size } bytes, invalid @ reference" ,
138+ threshold = ">50 bytes or valid @ reference" ,
139+ evidence = [
140+ f"CLAUDE.md exists but is minimal ({ size } bytes)" ,
141+ f"@ reference to { referenced_file } but file is missing or too small" ,
142+ ],
143+ remediation = self ._create_remediation (),
144+ error_message = None ,
145+ )
146+
147+ # File is small and no valid @ reference
70148 return Finding (
71149 attribute = self .attribute ,
72- status = "pass " ,
73- score = 100 .0 ,
74- measured_value = "present " ,
75- threshold = "present " ,
76- evidence = [f"CLAUDE.md found at { claude_md_path } " ],
77- remediation = None ,
150+ status = "fail " ,
151+ score = 25 .0 ,
152+ measured_value = f" { size } bytes " ,
153+ threshold = ">50 bytes " ,
154+ evidence = [f"CLAUDE.md exists but is minimal ( { size } bytes) " ],
155+ remediation = self . _create_remediation () ,
78156 error_message = None ,
79157 )
80158
81159 except FileNotFoundError :
160+ # CLAUDE.md not found - check for AGENTS.md as alternative
161+ agents_content , agents_size = self ._read_referenced_file (agents_md_path )
162+
163+ if agents_content and agents_size >= 50 :
164+ return Finding (
165+ attribute = self .attribute ,
166+ status = "pass" ,
167+ score = 90.0 , # Slightly lower score for missing CLAUDE.md
168+ measured_value = "AGENTS.md present" ,
169+ threshold = "CLAUDE.md or AGENTS.md" ,
170+ evidence = [
171+ "CLAUDE.md not found" ,
172+ f"AGENTS.md found with { agents_size } bytes (alternative)" ,
173+ "Consider adding CLAUDE.md as symlink or @ reference for broader tool support" ,
174+ ],
175+ remediation = None ,
176+ error_message = None ,
177+ )
178+
179+ # Neither file exists
82180 return Finding (
83181 attribute = self .attribute ,
84182 status = "fail" ,
85183 score = 0.0 ,
86184 measured_value = "missing" ,
87185 threshold = "present" ,
88- evidence = ["CLAUDE.md not found in repository root" ],
186+ evidence = [
187+ "CLAUDE.md not found in repository root" ,
188+ "AGENTS.md not found (alternative)" ,
189+ ],
89190 remediation = self ._create_remediation (),
90191 error_message = None ,
91192 )
@@ -94,12 +195,57 @@ def assess(self, repository: Repository) -> Finding:
94195 self .attribute , reason = f"Could not read CLAUDE.md file: { e } "
95196 )
96197
198+ def _extract_at_reference (self , content : str ) -> str | None :
199+ """Extract @ reference from CLAUDE.md content.
200+
201+ Looks for patterns like:
202+ - @AGENTS.md
203+ - @.claude/agents.md
204+ - @ AGENTS.md (with space)
205+
206+ Security: Rejects path traversal attempts (../) and absolute paths.
207+
208+ Returns the referenced filename or None if no reference found.
209+ """
210+ # Match @filename.md or @ filename.md (with optional space)
211+ # Support paths like @.claude/agents.md
212+ pattern = r"@\s*([A-Za-z0-9_\-./]+\.md)"
213+ match = re .search (pattern , content , re .IGNORECASE )
214+
215+ if match :
216+ ref = match .group (1 )
217+ # Prevent path traversal - reject if contains .. or starts with /
218+ if ".." in ref or ref .startswith ("/" ):
219+ return None
220+ return ref
221+ return None
222+
223+ def _read_referenced_file (self , file_path : Path ) -> tuple [str | None , int ]:
224+ """Read a referenced file and return its content and size.
225+
226+ Returns (content, size) tuple, or (None, 0) if file doesn't exist or can't be read.
227+ """
228+ try :
229+ with open (file_path , "r" , encoding = "utf-8" ) as f :
230+ content = f .read ()
231+ return content , len (content )
232+ except (FileNotFoundError , OSError , UnicodeDecodeError ):
233+ return None , 0
234+
235+ def _check_agents_md_exists (self , agents_md_path : Path ) -> bool :
236+ """Check if AGENTS.md exists and has sufficient content."""
237+ content , size = self ._read_referenced_file (agents_md_path )
238+ return content is not None and size >= 50
239+
97240 def _create_remediation (self ) -> Remediation :
98241 """Create remediation guidance for missing/inadequate CLAUDE.md."""
99242 return Remediation (
100- summary = "Create CLAUDE.md file with project-specific configuration for Claude Code " ,
243+ summary = "Create CLAUDE.md or AGENTS.md with project-specific configuration for AI coding assistants " ,
101244 steps = [
102- "Create CLAUDE.md file in repository root" ,
245+ "Choose one of three approaches:" ,
246+ " Option 1: Create standalone CLAUDE.md (>50 bytes) with project context" ,
247+ " Option 2: Create AGENTS.md and symlink CLAUDE.md to it (cross-tool compatibility)" ,
248+ " Option 3: Create AGENTS.md and reference it with @AGENTS.md in minimal CLAUDE.md" ,
103249 "Add project overview and purpose" ,
104250 "Document key architectural patterns" ,
105251 "Specify coding standards and conventions" ,
@@ -108,11 +254,22 @@ def _create_remediation(self) -> Remediation:
108254 ],
109255 tools = [],
110256 commands = [
257+ "# Option 1: Standalone CLAUDE.md" ,
111258 "touch CLAUDE.md" ,
112259 "# Add content describing your project" ,
260+ "" ,
261+ "# Option 2: Symlink CLAUDE.md to AGENTS.md" ,
262+ "touch AGENTS.md" ,
263+ "# Add content to AGENTS.md" ,
264+ "ln -s AGENTS.md CLAUDE.md" ,
265+ "" ,
266+ "# Option 3: @ reference in CLAUDE.md" ,
267+ "echo '@AGENTS.md' > CLAUDE.md" ,
268+ "touch AGENTS.md" ,
269+ "# Add content to AGENTS.md" ,
113270 ],
114271 examples = [
115- """# My Project
272+ """# Standalone CLAUDE.md (Option 1)
116273
117274## Overview
118275Brief description of what this project does.
@@ -136,15 +293,54 @@ def _create_remediation(self) -> Remediation:
136293- Use TypeScript strict mode
137294- Follow ESLint configuration
138295- Write tests for new features
139- """
296+ """ ,
297+ """# CLAUDE.md with @ reference (Option 3)
298+ @AGENTS.md
299+ """ ,
300+ """# AGENTS.md (shared by multiple tools)
301+
302+ ## Project Overview
303+ This project implements a REST API for user management.
304+
305+ ## Architecture
306+ - Layered architecture: controllers, services, repositories
307+ - PostgreSQL database with SQLAlchemy ORM
308+ - FastAPI web framework
309+
310+ ## Development Workflow
311+ ```bash
312+ # Setup
313+ python -m venv .venv
314+ source .venv/bin/activate
315+ pip install -e .
316+
317+ # Run tests
318+ pytest
319+
320+ # Start server
321+ uvicorn app.main:app --reload
322+ ```
323+
324+ ## Code Conventions
325+ - Use type hints for all functions
326+ - Follow PEP 8 style guide
327+ - Write docstrings for public APIs
328+ - Maintain >80% test coverage
329+ """ ,
140330 ],
141331 citations = [
142332 Citation (
143333 source = "Anthropic" ,
144334 title = "Claude Code Documentation" ,
145335 url = "https://docs.anthropic.com/claude-code" ,
146336 relevance = "Official guidance on CLAUDE.md configuration" ,
147- )
337+ ),
338+ Citation (
339+ source = "agents.md" ,
340+ title = "AGENTS.md Specification" ,
341+ url = "https://agents.md/" ,
342+ relevance = "Emerging standard for cross-tool AI assistant configuration" ,
343+ ),
148344 ],
149345 )
150346
0 commit comments