Development
Hook
Hooks
What letta-code runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
$ npx -y skills add letta-ai/letta-code --agent claude-codeShips with letta-code. Installing the plugin gets these hooks.
Where it lives
- hooks/block-rm-rf.shGitHub
Read the script
#!/bin/bash # Block dangerous rm -rf commands input=$(cat) tool_name=$(echo "$input" | jq -r '.tool_name') # Only check Bash commands if [ "$tool_name" != "Bash" ]; then exit 0 fi command=$(echo "$input" | jq -r '.tool_input.command') # Check for rm -rf pattern (handles -rf, -fr, -rfi, etc.) if echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)'; then echo "Blocked: rm -rf commands must be ran manually, use rm and rmdir instead." >&2 exit 2 fi exit 0
- hooks/desktop-notification.shGitHub
Read the script
#!/bin/bash # Send desktop notification using osascript (macOS) input=$(cat) message=$(echo "$input" | jq -r '.message') level=$(echo "$input" | jq -r '.level') # Display the notification (show subtitle only for warning/error) if [ "$level" = "error" ]; then osascript -e "display notification \"$message\" with title \"Letta Code\" subtitle \"Error\"" elif [ "$level" = "warning" ]; then osascript -e "display notification \"$message\" with title \"Letta Code\" subtitle \"Warning\"" else osascript -e "display notification \"$message\" with title \"Letta Code\"" fi exit 0
- hooks/fix-on-changes.shGitHub
Read the script
#!/bin/bash # Hook script: Run bun run fix if there are uncommitted changes # Triggered on: Stop event # Check if there are any uncommitted changes (staged or unstaged) if git diff --quiet HEAD 2>/dev/null; then echo "No changes, skipping." exit 0 fi # Run fix - capture output and send to stderr on failure output=$(bun run fix 2>&1) exit_code=$? if [ $exit_code -eq 0 ]; then echo "$output" exit 0 else echo "$output" >&2 exit 2 fi - hooks/get-api-key.tsGitHub
Read the script
#!/usr/bin/env bun // Helper script to get API key from keychain using Bun's secrets API // Used by memory_logger.py to avoid separate keychain authorization const SERVICE_NAME = "letta-code"; const API_KEY_NAME = "letta-api-key"; try { const apiKey = await Bun.secrets.get({ service: SERVICE_NAME, name: API_KEY_NAME, }); if (apiKey) { process.stdout.write(apiKey); } } catch { // Silent failure - Python will try other sources } - hooks/memory_logger.pyGitHub
Read the script
#!/usr/bin/env python3 """ Memory Logger Hook - Tracks memory block changes with git-style diffs. Structure: .letta/memory_logs/ human.json # Current state from server human.jsonl # Log of diffs (git-style patches) persona.json persona.jsonl ... Hook: Fetches all memory blocks, compares to local state, logs diffs. CLI: list - Show all memory blocks show <name> - Show current contents of a block history <name> - Interactive diff navigation """ import json import os import re import sys import difflib from datetime import datetime, timezone from pathlib import Path from typing import Optional import urllib.request import urllib.error # ============================================================================= # Configuration # ============================================================================= def get_logs_dir(working_dir: Optional[str] = None) -> Path: """Get the memory logs directory.""" if working_dir: return Path(working_dir) / ".letta" / "memory_logs" # For CLI usage, look relative to the script's parent directory (project root) # since the script lives in /hooks/memory_logger.py script_dir = Path(__file__).parent project_root = script_dir.parent return project_root / ".letta" / "memory_logs" def get_letta_settings() -> dict: """Read Letta settings from ~/.letta/settings.json.""" settings_path = Path.home() / ".letta" / "settings.json" if settings_path.exists(): try: return json.loads(settings_path.read_text()) except (json.JSONDecodeError, IOError): pass return {} def get_api_key_from_keychain() -> Optional[str]: """Get the Letta API key from macOS keychain via Bun helper.""" import subprocess # Use Bun helper script (uses Bun's existing keychain access) hooks_dir = Path(__file__).parent helper_script = hooks_dir / "get-api-key.ts" if helper_script.exists(): try: result = subprocess.run( ["bun", str(helper_script)], capture_output=True, text=True, timeout=5, ) if result.returncode == 0 and result.stdout.strip(): return result.stdout.strip() except (subprocess.TimeoutExpired, FileNotFoundError, OSError): pass return None def get_api_key() -> Optional[str]: """Get the Letta API key from keychain, environment, or settings.""" # Try macOS keychain first api_key = get_api_key_from_keychain() if api_key: return api_key # Fall back to environment variable api_key = os.environ.get("LETTA_API_KEY") if api_key: return api_key # Fall back to settings file settings = get_letta_settings() env_settings = settings.get("env", {}) return env_settings.get("LETTA_API_KEY") def get_base_url() -> str: """Get the Letta API base URL.""" base_url = os.environ.get("LETTA_BASE_URL") if base_url: return base_url.rstrip("/") settings = get_letta_settings() env_settings = settings.get("env", {}) return env_settings.get("LETTA_BASE_URL", "https://api.letta.com").rstrip("/") # ============================================================================= # Letta API # ============================================================================= def fetch_all_memory_blocks(agent_id: str, verbose: bool = False) -> list[dict]: """Fetch all memory blocks for an agent from the Letta API.""" api_key = get_api_key() base_url = get_base_url() if not api_key: if verbose: print(" ERROR: No API key available") return [] url = f"{base_url}/v1/agents/{agent_id}/core-memory/blocks" if verbose: print(f" URL: {url}") headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } try: req = urllib.request.Request(url, headers=headers, method="GET") with urllib.request.urlopen(req, timeout=10) as response: data = json.loads(response.read().decode("utf-8")) if verbose: print(f" Response type: {type(data).__name__}") return data if isinstance(data, list) else [] except urllib.error.HTTPError as e: if verbose: print(f" HTTP Error: {e.code} {e.reason}") try: body = e.read().decode("utf-8") print(f" Response: {body[:200]}") except Exception: pass return [] except (urllib.error.URLError, json.JSONDecodeError, TimeoutError) as e: if verbose: print(f" Error: {type(e).__name__}: {e}") return [] except Exception as e: if verbose: print(f" Unexpected error: {type(e).__name__}: {e}") return [] # ============================================================================= # Diff Operations # ============================================================================= def create_unified_diff(old_content: str, new_content: str, block_name: str) -> str: """Create a unified diff between old and new content.""" old_lines = old_content.splitlines(keepends=True) new_lines = new_content.splitlines(keepends=True) # Ensure trailing newlines for proper diff if old_lines and not old_lines[-1].endswith('\n'): old_lines[-1] += '\n' if new_lines and not new_lines[-1].endswith('\n'): new_lines[-1] += '\n' diff = difflib.unified_diff( old_lines, new_lines, fromfile=f"a/{block_name}", tofile=f"b/{block_name}", ) return "".join(diff) def apply_diff(content: str, diff_text: str, reverse: bool = False) -> str: """Apply or reverse a unified diff (pure Python implementation).""" lines = content.splitlines() diff_lines = diff_text.splitlines() # Parse hunks from d - hooks/permissions-status.shGitHub
Read the script
#!/bin/bash # Show current permission status when a permission request is made input=$(cat) tool_name=$(echo "$input" | jq -r '.tool_name') working_dir=$(echo "$input" | jq -r '.working_directory') # Colors for output BOLD='\033[1m' DIM='\033[2m' GREEN='\033[32m' RED='\033[31m' YELLOW='\033[33m' BLUE='\033[34m' RESET='\033[0m' echo -e "\n" echo -e "${BOLD}═══════════════════════════════════════════════════════════════${RESET}" echo -e "${BOLD}Permission Request: ${BLUE}$tool_name${RESET}" echo -e "${BOLD}═══════════════════════════════════════════════════════════════${RESET}\n" # Function to display permissions from a file show_permissions() { local file="$1" local label="$2" local color="$3" echo -e "${color}${BOLD}$label${RESET}" echo -e "${DIM}$file${RESET}" if [ -f "$file" ]; then local allow=$(jq -r '.permissions.allow // [] | .[]' "$file" 2>/dev/null) local deny=$(jq -r '.permissions.deny // [] | .[]' "$file" 2>/dev/null) local ask=$(jq -r '.permissions.ask // [] | .[]' "$file" 2>/dev/null) if [ -n "$allow" ] || [ -n "$deny" ] || [ -n "$ask" ]; then if [ -n "$allow" ]; then echo -e " ${GREEN}Allow:${RESET}" echo "$allow" | while read -r rule; do [ -n "$rule" ] && echo -e " ${GREEN}✓${RESET} $rule" done fi if [ -n "$deny" ]; then echo -e " ${RED}Deny:${RESET}" echo "$deny" | while read -r rule; do [ -n "$rule" ] && echo -e " ${RED}✗${RESET} $rule" done fi if [ -n "$ask" ]; then echo -e " ${YELLOW}Ask:${RESET}" echo "$ask" | while read -r rule; do [ -n "$rule" ] && echo -e " ${YELLOW}?${RESET} $rule" done fi else echo -e " ${DIM}(none)${RESET}" fi else echo -e " ${DIM}(file not found)${RESET}" fi echo "" } # XDG config settings (~/.config/letta/settings.json) xdg_config="${XDG_CONFIG_HOME:-$HOME/.config}" show_permissions "$xdg_config/letta/settings.json" "User Settings (XDG)" "$BLUE" # Legacy global settings (~/.letta/settings.json) show_permissions "$HOME/.letta/settings.json" "User Settings (Legacy)" "$BLUE" # Project settings (.letta/settings.json) show_permissions "$working_dir/.letta/settings.json" "Project Settings" "$YELLOW" # Project local settings (.letta/settings.local.json) show_permissions "$working_dir/.letta/settings.local.json" "Project Local Settings" "$GREEN" echo -e "${BOLD}═══════════════════════════════════════════════════════════════${RESET}\n" # Exit with code 1 to continue to normal permission flow (don't auto-allow/deny) exit 1 - hooks/prompt-instructions.shGitHub
- hooks/typecheck-on-changes.shGitHub
All 8 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withletta-code
Letta Code is a stateful agent harness for creating agents that are more like people than tools. Letta Code agents have memory, identity, and a sense of experience over time.
Get the whole plugin
Stats
2,976
Stars
349
Forks
Active
Maintenance
TypeScript
Language
Apache-2.0
License
2h ago
Last commit
9mo ago
Created
Repo: letta-ai/letta-code

