Static concurrency and race-condition auditor for Go, Java, Python, Node.js/TypeScript. Detects shared-state mutations without locks (Go map races, Java non-...
---
name: Concurrency Audit
description: Static concurrency and race-condition auditor for Go, Java, Python, Node.js/TypeScript. Detects shared-state mutations without locks (Go map races, Java non-atomic increments, Python thread/asyncio shared lists), TOCTOU (time-of-check-time-of-use) patterns across all languages, sync.Mutex copied by value, WaitGroup.Add inside goroutines, SimpleDateFormat as instance field, double-checked locking without volatile, asyncio shared state without Lock. Maps findings to CWE-362 (race condition) and CWE-367 (TOCTOU). Zero competitors on ClawHub — not a single concurrency-audit SKILL.md in 13,700+ files.
license: Apache-2.0
homepage: https://canlah.ai
tags:
- concurrency
- race-condition
- toctou
- go
- java
- python
- nodejs
- thread-safety
- correctness
metadata:
author: Canlah AI
version: "1.0.3"
---
# phy-concurrency-audit
Static scanner for **race conditions** (CWE-362) and **TOCTOU vulnerabilities** (CWE-367) in Go, Java, Python, and Node.js/TypeScript codebases. Finds shared-state mutations without synchronization, unsafe concurrency patterns, and check-then-act anti-patterns. Zero external API calls, zero dependencies beyond Python 3 stdlib.
## Why Concurrency Bugs Are Special
- **Non-deterministic**: reproduce only under load, on multi-core machines, or on specific OS schedulers
- **Silent corruption**: data races don't crash immediately — they corrupt state silently for hours
- **Security impact**: TOCTOU races in file ops enable symlink attacks and privilege escalation (CWE-367)
- **Hard to test**: even `go test -race` only finds races that actually execute during the test run
- Static analysis catches the ones that never trigger in testing
## What It Detects
### Go Race Conditions
| Pattern | Severity | CWE |
|---------|----------|-----|
| Global `map` variable read/written in concurrent functions | CRITICAL | CWE-362 |
| `sync.Mutex` or `sync.RWMutex` copied by value after first use | HIGH | CWE-362 |
| `sync.WaitGroup.Add()` called inside a goroutine | HIGH | CWE-362 |
| Package-level slice/map mutated without mutex | HIGH | CWE-362 |
| `atomic.Value` used without checking type consistency | MEDIUM | CWE-362 |
| Missing `sync.Once` for lazy singleton initialization | MEDIUM | CWE-362 |
| Struct fields accessed in goroutines without receiver mutex | HIGH | CWE-362 |
### Java Thread-Safety Violations
| Pattern | Severity | CWE |
|---------|----------|-----|
| `count++` / `i++` on shared instance/static field without `synchronized` or `AtomicInteger` | HIGH | CWE-362 |
| `HashMap` or `ArrayList` as instance field in class with concurrent methods | HIGH | CWE-362 |
| `SimpleDateFormat` as `static` or instance field (not thread-safe) | HIGH | CWE-362 |
| Double-checked locking without `volatile` keyword | HIGH | CWE-362 |
| `synchronized` block on non-final field (lock can change) | MEDIUM | CWE-362 |
| `Vector`/`Hashtable` used (thread-safe but deprecated, miss modern happens-before) | LOW | CWE-362 |
| `Calendar.getInstance()` result stored as field | MEDIUM | CWE-362 |
### Python Threading & Asyncio
| Pattern | Severity | CWE |
|---------|----------|-----|
| Thread function mutating module-level list/dict without `threading.Lock` | HIGH | CWE-362 |
| `counter += 1` / `list.append()` on global var in thread without lock | HIGH | CWE-362 |
| `asyncio` coroutine mutating shared object without `asyncio.Lock` | HIGH | CWE-362 |
| `multiprocessing.Process` sharing mutable list/dict without `Manager` | HIGH | CWE-362 |
| `threading.Thread` target accessing `global` without lock | HIGH | CWE-362 |
### Node.js / TypeScript Async Races
| Pattern | Severity | CWE |
|---------|----------|-----|
| Module-level object/array mutated inside `async` request handler | HIGH | CWE-362 |
| TOCTOU: `await cache.has(key)` followed by `await cache.get(key)` | HIGH | CWE-367 |
| Counter increment `count++` in async handler (non-atomic) | MEDIUM | CWE-362 |
| Promise chain modifying shared closure variable | MEDIUM | CWE-362 |
### TOCTOU Patterns (All Languages)
| Pattern | Severity | CWE |
|---------|----------|-----|
| `os.path.exists(path)` followed by `open(path)` without lock | HIGH | CWE-367 |
| `os.path.isdir(d)` then `os.makedirs(d)` without exist_ok=True | MEDIUM | CWE-367 |
| `os.path.exists` then `os.rename/remove/chmod` | HIGH | CWE-367 |
| `if not User.exists()` then `User.create()` — DB check-then-insert | HIGH | CWE-367 |
| Java `File.exists()` then `new FileOutputStream(file)` | HIGH | CWE-367 |
| Node.js `fs.existsSync()` then `fs.writeFileSync()` | HIGH | CWE-367 |
| Go `os.Stat(path)` check then `os.Open(path)` | HIGH | CWE-367 |
## Implementation
```python
#!/usr/bin/env python3
"""
phy-concurrency-audit — Race condition & TOCTOU static scanner
Usage: python3 audit_concurrency.py [path] [--json] [--ci]
"""
import argparse
import json
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
CRITICAL, HIGH, MEDIUM, LOW = "CRITICAL", "HIGH", "MEDIUM", "LOW"
SEV_ORDER = {CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3}
ICONS = {CRITICAL: "🔴", HIGH: "🟠", MEDIUM: "🟡", LOW: "⚪"}
@dataclass
class Finding:
file: str
line: int
pattern_name: str
matched_text: str
severity: str
cwe: str
title: str
detail: str
fix: str
# ─── Pattern registry ────────────────────────────────────────────────────────
PATTERNS: dict[str, list] = {
".go": [
("GO_GLOBAL_MAP_RACE",
re.compile(r'^var\s+\w+\s*=\s*(?:map\[|make\s*\(\s*map)', re.MULTILINE),
HIGH, "CWE-362",
"Package-level map — likely shared across goroutines without mutex",
"Go maps are NOT goroutine-safe. Any concurrent read+write is a data race that can panic.",
"Protect with sync.RWMutex or replace with sync.Map for concurrent access."),
("GO_MUTEX_COPY",
re.compile(r'\b(?:sync\.Mutex|sync\.RWMutex)\b'),
HIGH, "CWE-362",
"sync.Mutex/RWMutex — verify it is not copied after first use",
"Copying a Mutex after first Lock/Unlock produces a copy of a locked mutex — deadlock.",
"Always pass Mutex by pointer (*sync.Mutex) or embed in struct accessed via pointer."),
("GO_WAITGROUP_ADD_IN_GOROUTINE",
re.compile(r'go\s+func\s*\([^)]*\)\s*\{[^}]*wg\.Add\s*\(', re.DOTALL),
HIGH, "CWE-362",
"WaitGroup.Add() inside goroutine — race between Add and Wait",
"If wg.Wait() is called before wg.Add() completes, Wait returns prematurely.",
"Always call wg.Add(n) BEFORE launching goroutines, not inside them."),
("GO_ATOMIC_VALUE_MISUSE",
re.compile(r'\batomic\.Value\b'),
MEDIUM, "CWE-362",
"atomic.Value — verify consistent type across Store/Load calls",
"Storing different types in the same atomic.Value panics at runtime.",
"Ensure all Store() calls use the same concrete type; document the stored type in a comment."),
],
".java": [
("JAVA_NON_ATOMIC_INCREMENT",
re.compile(r'(?:private|protected|public|static)\s+(?:int|long|volatile\s+int|volatile\s+long)\s+\w+\s*(?:=\s*\d+)?\s*;'),
HIGH, "CWE-362",
"Shared numeric field — verify increments use AtomicInteger/AtomicLong",
"count++ on a shared field is a read-modify-write that is NOT atomic — silently loses updates.",
"Replace int count with AtomicInteger count = new AtomicInteger(0); use count.incrementAndGet()."),
("JAVA_SHARED_HASHMAP",
re.compile(r'(?:private|protected|public)\s+(?:final\s+)?HashMap\s*<'),
HIGH, "CWE-362",
"HashMap as instance field — not thread-safe for concurrent access",
"Concurrent reads+writes to HashMap can cause infinite loop (Java 7) or data loss (Java 8+).",
"Replace with ConcurrentHashMap or wrap access with synchronized blocks."),
("JAVA_SHARED_ARRAYLIST",
re.compile(r'(?:private|protected|public)\s+(?:final\s+)?ArrayList\s*<'),
HIGH, "CWE-362",
"ArrayList as instance field — not thread-safe for concurrent modification",
"Concurrent modification throws ConcurrentModificationException or silently drops elements.",
"Replace with CopyOnWriteArrayList or Collections.synchronizedList(new ArrayList<>())."),
("JAVA_SIMPLE_DATE_FORMAT",
re.compile(r'(?:private|protected|public|static)\s+(?:final\s+)?SimpleDateFormat\b'),
HIGH, "CWE-362",
"SimpleDateFormat as instance/static field — not thread-safe",
"SimpleDateFormat maintains mutable state internally — concurrent use causes wrong dates or exceptions.",
"Use ThreadLocal<SimpleDateFormat> or replace with thread-safe DateTimeFormatter (Java 8+)."),
("JAVA_DOUBLE_CHECKED_LOCKING",
re.compile(r'if\s*\(\s*\w+\s*==\s*null\s*\)\s*\{\s*synchronized\s*\('),
HIGH, "CWE-362",
"Double-checked locking without volatile — broken without memory barrier",
"Without volatile, the JIT can reorder writes; the second thread sees a partially constructed object.",
"Add 'volatile' to the field declaration: private volatile MyClass instance;"),
("JAVA_SYNC_ON_NONFINAL",
re.compile(r'synchronized\s*\(\s*(?!this|class)\w+\s*\)'),
MEDIUM, "CWE-362",
"synchronized() on potentially non-final field — lock can change",
"If the locked object's reference changes between synchronized blocks, protection is lost.",
"Only synchronize on final fields, 'this', or class literals."),
],
".py": [
("PY_THREAD_GLOBAL_MUTATION",
re.compile(r'def\s+\w+\s*\([^)]*\)\s*:\s*(?:[^\n]*\n\s*)*?global\s+\w+', re.MULTILINE),
HIGH, "CWE-362",
"Thread function using 'global' — likely shared state without lock",
"Global mutations in threads (counter += 1, list.append) are not atomic in CPython despite the GIL.",
"Protect with threading.Lock(): with lock: global_var += 1"),
("PY_THREAD_LIST_MUTATION",
re.compile(r'threading\.Thread\s*\([^)]*target\s*=\s*\w+'),
MEDIUM, "CWE-362",
"threading.Thread — verify shared list/dict access is protected by threading.Lock()",
"Compound operations on shared containers (check-then-modify) are not atomic under GIL.",
"Use threading.Lock() or queue.Queue for thread-safe producer/consumer patterns."),
("PY_ASYNCIO_SHARED_MUTATION",
re.compile(r'async\s+def\s+\w+.*?(?:\n.*?){0,10}(?:shared|cache|results|state)\s*[\[.]=', re.DOTALL),
HIGH, "CWE-362",
"asyncio coroutine mutating shared variable — TOCTOU risk at await boundaries",
"Between two await points, another coroutine can run and modify the same shared state.",
"Use asyncio.Lock() to protect read-modify-write sequences across await boundaries."),
("PY_TOCTOU_EXISTS_OPEN",
re.compile(r'os\.path\.exists\s*\([^)]+\)(?:[^\n]*\n){0,5}[^\n]*\bopen\s*\(', re.MULTILINE),
HIGH, "CWE-367",
"TOCTOU: os.path.exists() followed by open() — race window for file swap",
"An attacker can replace the file with a symlink between exists() check and open() call.",
"Remove exists() check; use try/except FileNotFoundError on open() directly (atomic)."),
("PY_TOCTOU_ISDIR_MAKEDIRS",
re.compile(r'os\.path\.isdir\s*\([^)]+\)(?:[^\n]*\n){0,5}[^\n]*os\.makedirs\s*\('),
MEDIUM, "CWE-367",
"TOCTOU: isdir() + makedirs() — another process can create dir between checks",
"Race causes FileExistsError even though the directory now exists.",
"Use os.makedirs(path, exist_ok=True) — single atomic operation."),
("PY_TOCTOU_EXISTS_RENAME",
re.compile(r'os\.path\.exists\s*\([^)]+\)(?:[^\n]*\n){0,5}[^\n]*(?:os\.rename|os\.remove|os\.chmod|shutil\.move)\s*\('),
HIGH, "CWE-367",
"TOCTOU: exists() check then file operation — file can be replaced between calls",
"Symlink attack: attacker replaces target file with symlink to sensitive file before chmod/rename.",
"Use try/except on the operation directly; or use os.open() with O_NOFOLLOW flag for security-sensitive ops."),
],
".js": _build_js_concurrency_patterns(),
".ts": _build_js_concurrency_patterns(),
}
def _build_js_concurrency_patterns():
return [
("JS_MODULE_STATE_ASYNC_MUTATION",
re.compile(r'(?:const|let|var)\s+\w+\s*=\s*(?:\{|\[)(?:[^;]*\n){0,5}.*?(?:app|router|server)\.[a-z]+\s*\(.*?\+\+|\s+\w+\.push\(|\s+\w+\[', re.DOTALL),
MEDIUM, "CWE-362",
"Module-level state mutated in request handler — concurrent requests race",
"Node.js is single-threaded but async: two requests can interleave at await points, corrupting shared state.",
"Move state into request scope, use a database/cache, or protect with async-mutex library."),
("JS_TOCTOU_CACHE_HAS_GET",
re.compile(r'await\s+\w+\.has\s*\([^)]+\)(?:[^\n]*\n){0,5}[^\n]*await\s+\w+\.get\s*\('),
HIGH, "CWE-367",
"TOCTOU: await cache.has() followed by await cache.get() — key can be deleted between awaits",
"Another async operation can delete the cache key after has() returns true but before get().",
"Use atomic getOrCreate pattern: const val = cache.get(key) ?? (await compute()); cache.set(key, val);"),
("JS_COUNTER_ASYNC",
re.compile(r'(?:let|var)\s+\w*(?:count|counter|total|hits)\w*\s*=\s*0(?:[^;]*\n){0,20}.*?\+\+', re.DOTALL),
MEDIUM, "CWE-362",
"Counter incremented across async boundaries — not atomic",
"count++ is not atomic when two requests execute counter++ concurrently at an await boundary.",
"Use a single-tick increment (no await between read and write), or use an atomic counter library."),
("JS_TOCTOU_FS_EXISTS_WRITE",
re.compile(r'(?:fs\.existsSync|await\s+fs\.(?:promises\.)?access)\s*\([^)]+\)(?:[^\n]*\n){0,5}[^\n]*(?:fs\.writeFileSync|await\s+fs\.(?:promises\.)?writeFile)\s*\('),
HIGH, "CWE-367",
"TOCTOU: fs.existsSync + fs.writeFile — race between check and write",
"Another process can create or modify the file between existsSync() and writeFile().",
"Use flags: fs.writeFileSync(path, data, {flag: 'wx'}) — fails atomically if file already exists."),
]
# Shared-state context helpers
GO_GOROUTINE_MARKER = re.compile(r'\bgo\s+(?:func|\w+)\s*\(')
JAVA_CONCURRENT_MARKER = re.compile(r'(?:Thread|Executor|CompletableFuture|@Async|synchronized|Runnable|Callable)\b')
PY_CONCURRENT_MARKER = re.compile(r'(?:threading|asyncio|concurrent\.futures|multiprocessing)\b')
JS_ASYNC_MARKER = re.compile(r'\basync\s+function|\bPromise\.all|\bawait\b')
CONCURRENCY_MARKERS = {
".go": GO_GOROUTINE_MARKER,
".java": JAVA_CONCURRENT_MARKER,
".py": PY_CONCURRENT_MARKER,
".js": JS_ASYNC_MARKER,
".ts": JS_ASYNC_MARKER,
}
SKIP_DIRS = {".git", "node_modules", "vendor", "__pycache__", ".venv", "venv",
"dist", "build", "target", "test", "tests", "__tests__", "spec"}
def scan_file(filepath: Path) -> list[Finding]:
suffix = filepath.suffix.lower()
if suffix not in PATTERNS:
return []
try:
lines = filepath.read_text(encoding="utf-8", errors="replace").splitlines()
except (OSError, PermissionError):
return []
full_text = "\n".join(lines)
# Skip files with no concurrency markers at all (reduces false positives significantly)
marker = CONCURRENCY_MARKERS.get(suffix)
if marker and not marker.search(full_text):
# Only skip for non-TOCTOU checks if truly no concurrent code
pass # Still scan for TOCTOU even in non-concurrent files
findings: list[Finding] = []
for (name, pat, base_sev, cwe, title, detail, fix) in PATTERNS[suffix]:
for m in pat.finditer(full_text):
lineno = full_text[:m.start()].count("\n") + 1
line_text = lines[lineno - 1]
findings.append(Finding(
file=str(filepath),
line=lineno,
pattern_name=name,
matched_text=line_text.strip()[:120],
severity=base_sev,
cwe=cwe,
title=title,
detail=detail,
fix=fix,
))
return findings
def walk_files(root: Path) -> list[Path]:
exts = {".go", ".java", ".py", ".js", ".ts"}
results = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
for fname in filenames:
if Path(fname).suffix.lower() in exts:
results.append(Path(dirpath) / fname)
return results
def format_report(findings: list[Finding], scanned: int) -> str:
by_sev = {CRITICAL: [], HIGH: [], MEDIUM: [], LOW: []}
for f in findings:
by_sev[f.severity].append(f)
lines = [
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
" CONCURRENCY AUDIT — Race Conditions & TOCTOU (CWE-362/367)",
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
f" Scanned: {scanned} files",
f" Findings: {len(by_sev[CRITICAL])} CRITICAL {len(by_sev[HIGH])} HIGH "
f"{len(by_sev[MEDIUM])} MEDIUM {len(by_sev[LOW])} LOW",
"",
]
for sev in [CRITICAL, HIGH, MEDIUM, LOW]:
group = by_sev[sev]
if not group:
continue
lines.append(f"{ICONS[sev]} {sev} ({len(group)} findings)")
lines.append("")
for f in sorted(group, key=lambda x: x.file):
rel = os.path.relpath(f.file)
lines += [
f" {rel}:{f.line} — {f.pattern_name}",
f" Code: {f.matched_text}",
f" Issue: {f.title}",
f" Why: {f.detail}",
f" {f.cwe}",
f" Fix: {f.fix}",
"",
]
critical_high = len(by_sev[CRITICAL]) + len(by_sev[HIGH])
lines += [
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
f" CI gate: {'exit 1 — race conditions detected' if critical_high else 'exit 0 — clean'}",
" Runtime verification: go test -race ./... | ThreadSanitizer",
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
]
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Race condition & TOCTOU scanner")
parser.add_argument("path", nargs="?", default=".", help="Root directory to scan")
parser.add_argument("--json", action="store_true")
parser.add_argument("--ci", action="store_true", help="Exit 1 if HIGH or CRITICAL found")
args = parser.parse_args()
files = walk_files(Path(args.path).resolve())
all_findings: list[Finding] = []
for f in files:
all_findings.extend(scan_file(f))
all_findings.sort(key=lambda x: (SEV_ORDER[x.severity], x.file, x.line))
if args.json:
import dataclasses
print(json.dumps([dataclasses.asdict(f) for f in all_findings], indent=2))
else:
print(format_report(all_findings, len(files)))
if args.ci:
has_high = any(f.severity in (CRITICAL, HIGH) for f in all_findings)
sys.exit(1 if has_high else 0)
if __name__ == "__main__":
main()
```
## Usage
```bash
# Scan current project
python3 audit_concurrency.py
# CI fail-gate (exits 1 on HIGH/CRITICAL)
python3 audit_concurrency.py --ci
# JSON output for pipeline integration
python3 audit_concurrency.py --json | jq '[.[] | select(.cwe == "CWE-367")]'
# GitHub Actions
- name: Concurrency Audit
run: python3 .claude/skills/phy-concurrency-audit/audit_concurrency.py --ci
```
## Sample Output
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CONCURRENCY AUDIT — Race Conditions & TOCTOU (CWE-362/367)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Scanned: 78 files
Findings: 0 CRITICAL 4 HIGH 2 MEDIUM 0 LOW
🟠 HIGH (4 findings)
internal/cache/store.go:12 — GO_GLOBAL_MAP_RACE
Code: var requestCache = map[string]*Response{}
Issue: Package-level map — likely shared across goroutines without mutex
Why: Go maps are NOT goroutine-safe. Concurrent read+write causes panic.
CWE-362
Fix: Protect with sync.RWMutex or replace with sync.Map for concurrent access.
src/DateParser.java:8 — JAVA_SIMPLE_DATE_FORMAT
Code: private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Issue: SimpleDateFormat as static field — not thread-safe
Why: SimpleDateFormat maintains mutable state; concurrent use corrupts date parsing.
CWE-362
Fix: Use ThreadLocal<SimpleDateFormat> or replace with DateTimeFormatter (Java 8+).
api/file_handler.py:34 — PY_TOCTOU_EXISTS_OPEN
Code: if os.path.exists(upload_path):
Issue: TOCTOU: os.path.exists() followed by open() — race window for file swap
Why: Attacker can replace file with symlink between exists() and open().
CWE-367
Fix: Remove exists() check; use try/except FileNotFoundError on open() directly.
routes/api.js:67 — JS_TOCTOU_CACHE_HAS_GET
Code: if (await redisClient.has(userId)) {
Issue: TOCTOU: await cache.has() followed by await cache.get()
Why: Key can be evicted between has() and get() under concurrent load.
CWE-367
Fix: Use atomic getOrCreate: const val = await cache.get(key) ?? compute();
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CI gate: exit 1 — race conditions detected
Runtime verification: go test -race ./... | ThreadSanitizer
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
## Relationship to Runtime Detectors
This skill does **static** analysis — fast, zero infrastructure, no load required. Use alongside:
| Tool | What It Adds |
|------|-------------|
| `go test -race ./...` | Finds Go races that actually execute during tests |
| Java ThreadSanitizer | Runtime instrumentation for JVM races |
| Python `threading-sanitizer` | Runtime detection for CPython |
| `node --inspect` + `clinic.js` | Profiles async bottlenecks + race-prone patterns |
## Companion Skills
| Skill | Use Together For |
|-------|-----------------|
| `phy-memory-leak-detector` | Goroutine/listener leaks + race conditions — concurrent correctness sweep |
| `phy-deserialization-audit` | Security sweep: races often affect auth/session state |
| `phy-k8s-security-audit` | Race conditions in init containers + probe handlers |
| `phy-ssrf-audit` | Async SSRF: race between URL validation and fetch |
---
## Author
**[Canlah AI](https://canlah.ai)** — Run performance marketing without breaking your brand.
- GitHub: [github.com/PHY041](https://github.com/PHY041)
- All Skills: [clawhub.ai/PHY041](https://clawhub.ai/PHY041)
don't have the plugin yet? install it then click "run inline in claude" again.