Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .claude/SKILL-WHITELIST.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"version": "1.0.0",
"description": "M-09: Trusted skills and sub-agents allowed for auto-injection. Items not in this list will be blocked with a security warning.",
"updated": "2026-03-01",
"trusted_skills": [
"agent-creation",
"ask-company",
"brainstorming",
"chronicler",
"code-review",
"convert-to-company-docs",
"dispatching-parallel-agents",
"docs-megabrain",
"executing-plans",
"executor",
"fase-2-5-tagging",
"feature-dev",
"frontend-design",
"gdrive-transcription-downloader",
"gemini-fallback",
"gha",
"github-workflow",
"hookify",
"hybrid-source-reading",
"jarvis",
"jarvis-briefing",
"knowledge-extraction",
"ler-planilha",
"pipeline-jarvis",
"plugin-dev",
"process-company-inbox",
"pr-review-toolkit",
"python-megabrain",
"resume",
"save",
"skill-creator-internal",
"skill-writer",
"smart-download-tagger",
"source-sync",
"sync-docs",
"using-superpools",
"using-superpowers",
"verification-before-completion",
"verify",
"verify-6-levels",
"writing-plans"
],
"trusted_subagents": [],
"blocked": []
}
139 changes: 139 additions & 0 deletions .claude/hooks/session_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import sys
import os
import re
import hashlib
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional, Dict, List, Any
Expand Down Expand Up @@ -115,6 +116,128 @@
}
}

#================================
# M-08: PERSONALITY FILE INTEGRITY
#================================

# Files injected into context that must be verified
PERSONALITY_FILES = [
'.claude/jarvis/JARVIS-DNA-PERSONALITY.md',
'system/02-JARVIS-SOUL.md',
'.claude/jarvis/JARVIS-BOOT-SEQUENCE.md',
'.claude/jarvis/JARVIS-MEMORY.md',
]

INTEGRITY_MANIFEST_PATH = '.claude/jarvis/INTEGRITY-MANIFEST.json'


def compute_file_hash(filepath: Path) -> Optional[str]:
"""Compute SHA256 hash of a file for integrity verification."""
try:
sha256 = hashlib.sha256()
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(8192), b''):
sha256.update(chunk)
return sha256.hexdigest()
except Exception:
return None


def load_integrity_manifest() -> Dict:
"""Load integrity manifest from disk."""
project_dir = get_project_dir()
manifest_path = Path(project_dir) / INTEGRITY_MANIFEST_PATH
if not manifest_path.exists():
return {}
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return {}


def save_integrity_manifest(manifest: Dict):
"""Save integrity manifest to disk."""
project_dir = get_project_dir()
manifest_path = Path(project_dir) / INTEGRITY_MANIFEST_PATH
manifest_path.parent.mkdir(parents=True, exist_ok=True)
try:
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
except Exception:
pass


def verify_personality_integrity() -> Dict:
"""
M-08: Verify integrity of personality files against manifest.
Warn-only: never blocks session start.

On first run: creates manifest with current hashes.
On subsequent runs: compares hashes, warns if changed.
Does NOT auto-update manifest when changes detected (requires manual reset).
"""
project_dir = get_project_dir()
manifest = load_integrity_manifest()
files_section = manifest.get('files', {})

result = {
'verified': [],
'changed': [],
'new_files': [],
'missing': [],
'manifest_exists': bool(files_section),
}

current_hashes = {}

for rel_path in PERSONALITY_FILES:
filepath = Path(project_dir) / rel_path
if not filepath.exists():
result['missing'].append(rel_path)
continue

current_hash = compute_file_hash(filepath)
if current_hash is None:
continue

current_hashes[rel_path] = current_hash

if rel_path in files_section:
expected_hash = files_section[rel_path].get('sha256')
if expected_hash and expected_hash != current_hash:
result['changed'].append(rel_path)
else:
result['verified'].append(rel_path)
else:
result['new_files'].append(rel_path)

now = datetime.now().isoformat()

if not files_section:
# First run: create manifest with current hashes (baseline)
new_manifest = {
'version': '1.0.0',
'description': 'Integrity manifest for personality files (M-08)',
'generated': now,
'last_verified': now,
'files': {}
}
for rel_path, file_hash in current_hashes.items():
new_manifest['files'][rel_path] = {
'sha256': file_hash,
'verified_at': now
}
save_integrity_manifest(new_manifest)
elif not result['changed']:
# No changes: update last_verified timestamp only
manifest['last_verified'] = now
save_integrity_manifest(manifest)
# If changes detected: DON'T update manifest - preserve old hashes for comparison

return result


#================================
# UTILITÁRIOS
#================================
Expand Down Expand Up @@ -836,6 +959,9 @@ def main():
# === VERIFICAR INTEGRIDADE ===
integrity = check_system_integrity()

# === M-08: VERIFY PERSONALITY FILE INTEGRITY ===
personality_integrity = verify_personality_integrity()

# === CARREGAR TODOS OS ARQUIVOS ===
state = load_state()
memory = load_memory_owner()
Expand Down Expand Up @@ -879,6 +1005,19 @@ def main():
if warnings:
output_parts.append(warnings)

# M-08: Personality integrity warnings
if personality_integrity.get('changed'):
output_parts.append("┌──────────────────────────────────────────────────────────────────────────────┐")
output_parts.append("│ ⚠️ PERSONALITY INTEGRITY WARNING │")
output_parts.append("├──────────────────────────────────────────────────────────────────────────────┤")
for changed_file in personality_integrity['changed']:
fname = changed_file.split('/')[-1][:50]
output_parts.append(f"│ Modified since last verified: {fname:<40}│")
output_parts.append("│ Run /verify-integrity to accept changes or investigate. │")
output_parts.append("└──────────────────────────────────────────────────────────────────────────────┘")
elif not personality_integrity.get('manifest_exists'):
output_parts.append("[INTEGRITY] First run: personality file manifest created.")

# Sistemas carregados
loaded = integrity.get('loaded', [])
output_parts.append(f"\n[SISTEMAS] {len(loaded)}/8 arquivos carregados: {', '.join(loaded[:5])}...")
Expand Down
92 changes: 92 additions & 0 deletions .claude/hooks/skill_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,21 @@
import os
import re
import json
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple

PROJECT_ROOT = Path(os.environ.get('CLAUDE_PROJECT_DIR', '.'))
SKILLS_PATH = PROJECT_ROOT / ".claude" / "skills"
SUBAGENTS_PATH = PROJECT_ROOT / ".claude" / "jarvis" / "sub-agents"
INDEX_PATH = PROJECT_ROOT / ".claude" / "mission-control" / "SKILL-INDEX.json"
WHITELIST_PATH = PROJECT_ROOT / ".claude" / "SKILL-WHITELIST.json"

# M-09: Allowed base directories for skill/sub-agent resolution
ALLOWED_SKILL_PREFIXES = (
os.path.normpath('.claude/skills/'),
os.path.normpath('.claude/jarvis/sub-agents/'),
)


def scan_skills() -> List[Tuple[Path, str]]:
Expand Down Expand Up @@ -277,6 +285,73 @@ def get_item_context(item_path: str, item_type: str) -> str:
return get_subagent_context(item_path)


#================================
# M-09: SECURITY - WHITELIST & PATH VALIDATION
#================================

def is_path_safe(item_path: str) -> bool:
"""
M-09: Validate that skill/sub-agent path doesn't escape expected directories.
Prevents path traversal attacks via crafted SKILL.md paths.
"""
normalized = os.path.normpath(item_path)
return any(normalized.startswith(prefix) for prefix in ALLOWED_SKILL_PREFIXES)


def load_whitelist() -> Dict:
"""Load skill/sub-agent whitelist. Empty dict = no whitelist (trust all)."""
if not WHITELIST_PATH.exists():
return {}
try:
with open(WHITELIST_PATH, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception:
return {}


def is_skill_trusted(item_name: str, item_type: str, whitelist: Dict) -> bool:
"""
M-09: Check if skill/sub-agent is in the trusted whitelist.
If no whitelist file exists, all items in valid paths are trusted (graceful degradation).
"""
if not whitelist:
return True # No whitelist file = trust all (backward compatible)

blocked_list = whitelist.get('blocked', [])
if item_name in blocked_list:
return False

if item_type == "skill":
trusted_list = whitelist.get('trusted_skills', [])
else:
trusted_list = whitelist.get('trusted_subagents', [])

if '*' in trusted_list:
return True # Wildcard = trust all of this type

return item_name in trusted_list


def log_security_event(event_type: str, item_name: str, item_path: str):
"""M-09: Log security events for skill routing."""
log_dir = PROJECT_ROOT / 'logs'
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / 'skill-security.jsonl'

entry = {
'timestamp': datetime.now().isoformat(),
'event': event_type,
'item_name': item_name,
'item_path': item_path
}

try:
with open(log_file, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
except Exception:
pass


def main():
"""
Hook entry point for Claude Code UserPromptSubmit event.
Expand All @@ -303,6 +378,23 @@ def main():
item_type = top.get('type', 'skill')
item_name = top.get('name', 'unknown')

# M-09: Path safety check — block path traversal
if not is_path_safe(top['path']):
log_security_event('path_traversal_blocked', item_name, top['path'])
print(json.dumps({'continue': True}))
return

# M-09: Whitelist check — block unwhitelisted items
whitelist = load_whitelist()
if not is_skill_trusted(item_name, item_type, whitelist):
log_security_event('unwhitelisted_blocked', item_name, top['path'])
type_label = "skill" if item_type == "skill" else "sub-agent"
print(json.dumps({
'continue': True,
'feedback': f"[SECURITY] {type_label} '{item_name}' not in trusted whitelist. Skipping auto-injection."
}))
return

context = get_item_context(top['path'], item_type)

if context:
Expand Down
Loading