T09 · Insecure Skill Coding Practices
- Location
scripts/analyze.py:423- Finding
Session Message Snippets Are Persisted in Plaintext and Retained Indefinitely in Archives
- Content
View full analysis
Vulnerability Details
File Location:
scripts/analyze.py:90-96, 118-125, 335-365, 423-430, 437-474
Vulnerability Type: Plaintext storage and excessive retention of potentially sensitive session data
Risk Level: MediumVulnerable Code
The analyzer recursively scans the user's Claude project sessions by default:
python parser.add_argument( "--session-dir", default=str(Path.home() / ".claude" / "projects"), help="Root directory for session JSONL files", )python def iter_session_files(session_dir: Path) -> Iterator[Path]: """Yield session JSONL files, skipping test/tmp directories.""" for path in session_dir.rglob("*.jsonl"): path_str = str(path) if "pytest" in path_str or "/tmp/" in path_str: continue if "/subagents/" in path_str: continue yield pathUser text is copied into a feedback event without secret detection or redaction:
python def _build_event( invocation: SkillInvocation, outcome: str, confidence: float, correction_type: str | None, user_text: str, turns_to_feedback: int, ai_tools: list[str], ) -> FeedbackEvent: event_id = hashlib.sha256( f"{invocation.invocation_id}:{invocation.skill_id}".encode() ).hexdigest()[:16] snippet = " ".join(user_text.split())[:200] dimension = attribute_dimension(snippet) return FeedbackEvent( event_id=event_id, timestamp=invocation.timestamp, session_id="", # filled by caller skill_id=invocation.skill_id, invocation_uuid=invocation.invocation_id, outcome=outcome, confidence=confidence, correction_type=correction_type, user_message_snippet=snippet, turns_to_feedback=turns_to_feedback, ai_tools_used=ai_tools, dimension_hint=dimension, )Snippets are written in plaintext unless the user explicitly selects
--no-snippets:python with output_path. ...[truncated 4645 chars]- Remediation
View remediation
Remediation Suggestions
-
Make privacy-preserving behavior the default:
- Do not persist message snippets unless the user explicitly supplies an opt-in option such as
--include-snippets. - Retain only the outcome, confidence, dimension, timestamp, and pseudonymous identifiers by default.
- Do not persist message snippets unless the user explicitly supplies an opt-in option such as
-
Add secret and personal-data redaction before persistence:
- Detect common API keys, bearer tokens, private-key blocks, credentials, email addresses, and high-entropy strings.
- Replace detected values with stable placeholders rather than storing raw text.
- Apply redaction before dimension attribution and serialization so sensitive values do not enter intermediate event objects.
-
Enforce restrictive storage permissions:
- Create output directories with owner-only permissions where supported.
- Open new feedback and archive files with mode
0600. - Verify existing output files are not group-readable or world-readable before appending.
-
Implement an actual retention policy:
- Delete or aggregate records after the configured retention period instead of moving raw snippets into permanent archives.
- Make retention duration configurable and document it clearly.
- If historical metrics are needed, archive only aggregated, snippet-free statistics.
-
Reduce collection scope:
- Require explicit project selection or provide a preview of session files before recursively scanning all projects.
- Support allowlists and exclusions resolved by path components rather than substring matching.
-
Protect existing data:
- Provide a migration or cleanup command that strips
user_message_snippetfrom active and archived JSONL files. - Warn users that older versions may already have created plaintext copies.
- Avoid placing the default output inside paths likely to be committed; add
feedback-store/to recommended ignore rules.
- Provide a migration or cleanup command that strips
-
Add security tests:
- Verify snippets are absent under default settings.
- Verify representative ...[truncated 160 chars]
-
