This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
TeamPass is a collaborative on-premise password manager built in PHP. It emphasizes security through multi-layer encryption, comprehensive audit logging, and granular role-based access control.
Tech Stack:
- PHP 8.1+ (strict typing throughout)
- MySQL 5.7+ / MariaDB 10.7+
- MeekroDB for database abstraction
- Symfony components (Session, PasswordHasher, HttpFoundation)
- AdminLTE 3 / Bootstrap / jQuery frontend
- Defuse PHP Encryption for data encryption
vendor/bin/phpstan analyse # PHPStan level 4
vendor/bin/composer-license-checker # License compliance
composer install # PHP dependencies
php scripts/background_tasks___handler.php # Background tasksDatabase schema: initial install via /install/install.php, upgrades via /install/upgrade.php + /install/upgrade_run_*.php.
Entry Points:
- Web:
/index.php→ page routing via?page=parameter - API:
/api/index.php→ JWT-authenticated REST - Install:
/install/install.phpor/install/upgrade.php
Request flow: index.php → /pages/*.php (HTML) → /pages/*.js.php (JS) → AJAX → /sources/*.queries.php (JSON response)
Directory Structure:
/sources/ - Backend AJAX handlers (*.queries.php) + core.php, identify.php, main.functions.php
/pages/ - Frontend templates (*.php) and JS (*.js.php)
/includes/ - Config, core libs, teampassclasses/
/api/ - REST API (Controller/Api/, Model/, inc/)
/install/ - Installer and upgrade scripts
/scripts/ - Background tasks (cron)
/vendor/ - Composer dependencies
Custom TeamPass Classes (in /includes/libraries/teampassclasses/, PSR-4):
SessionManager— Symfony Session + EncryptedSessionProxy (Redis opt-in or filesystem)ConfigManager— settings fromteampass_miscDB table, APCu-cached 60s; callinvalidateCache()after writesPasswordManager— bcrypt/argon2 via Symfony PasswordHasherEncryption— AES-256-CBC for client-server commsNestedTree— MPTT folder hierarchy (nleft/nright/nlevel); never update these columns manuallyCryptoManager— single entry point for all RSA/AES crypto; never call phpseclib directly
Dual-location classes: ConfigManager and SessionManager exist in both includes/libraries/teampassclasses/ and vendor/teampassclasses/. Always edit both.
DB::query('SELECT * FROM %l WHERE id=%i', 'teampass_users', $userId);
DB::queryFirstRow('SELECT * FROM %l WHERE id=%i', 'teampass_items', $itemId);
DB::insert('teampass_items', ['label' => 'Test', 'password' => $encrypted]);
DB::update('teampass_users', ['timestamp' => time()], 'id=%i', $userId);
DB::delete('teampass_log_items', 'id_item=%i', $itemId);
// %s=string %i=integer %l=literal(table/col) %ls=array for INKey Tables: teampass_users, teampass_items, teampass_nested_tree, teampass_misc, teampass_log_items, teampass_sharekeys_items, teampass_roles_title
Login Flow: login.php → sources/identify.php → DB/LDAP/OAuth2 validation → SessionManager::getSession() → set session vars → load encryption keys
Key session vars:
$session = SessionManager::getSession();
$session->get('user-id'); // User ID
$session->get('user-login'); // Username
$session->get('user-admin'); // Admin flag (1/0)
$session->get('user-roles'); // Semicolon-separated role IDs
$session->get('user-accessible_folders'); // Array of folder IDs
$session->get('user-privatekey'); // User's private encryption key
$session->get('user-session_duration'); // Expiration timestamp
$session->get('key'); // Random session encryption keySession validation on every request via sources/core.php (checks user-session_duration + key_tempo).
MFA: Google Authenticator (TOTP), Duo Security, YubiKey, AGSES.
Full architecture details: @.claude/docs/architecture-encryption.md
Rule: always use decryptUserObjectKeyWithMigration() in new code — never call rsaDecrypt() directly for sharekeys. This transparently upgrades phpseclib v1 → v3 on access.
Rule: applies to custom field sharekeys too — every read path on sharekeys_fields must use decryptUserObjectKeyWithMigration(). The SELECT must include increment_id.
Rule: always encrypt before INSERT for custom fields — never insert plaintext and update afterwards. A failed UPDATE leaves plaintext with encryption_type='not_set', silently bypassing decryption.
Encryption version: encryption_version=1 = phpseclib v1 (SHA-1/OAEP, legacy), encryption_version=3 = phpseclib v3 (SHA-256/OAEP, current).
Full architecture details: @.claude/docs/architecture-websocket.md
Rule: always call the high-level helpers (emitItemEvent, emitFolderEvent, etc.) after any write on items/folders in sources/*.queries.php. Never insert into teampass_websocket_events directly.
Full architecture details: @.claude/docs/architecture-php-fpm.md
Rule: spawn background tasks with getPHPBinary() — it resolves a real PHP CLI binary under FPM (never php-fpm / 'false'). Rule: tpFinishRequestEarly() only after the full response is echoed — later output is not delivered. Admin settings: cli_php_binary_path, enable_fastcgi_finish_request.
Full reference: @.claude/docs/api-reference.md
Controllers in /api/Controller/Api/. JWT auth via Authorization: Bearer <token>. Key endpoints: /api/authorize, /api/item/get, /api/item/create, /api/item/getOtp, /api/folder/listFolders.
Full architecture details: @.claude/docs/architecture-extension-autoconfig.md
One-click setup of the browser extension from the web app: a same-origin window.postMessage bridge detects the extension (content script on <all_urls>) and pushes a config bundle; a downloadable JSON file is the fallback. Credentials use token mode (a durable PAT) — the password is never transmitted.
Rule: both PAT gates (issuance in users.queries.php, consumption in AuthModel::getUserAuthByToken) are relaxed only behind the admin toggle extension_token_all_auth_types (default 0, Settings → API → Browser extension). Off ⇒ OAuth2-only behaviour preserved. Rule: the auto-config PAT is durable (expires_at = NULL), not single-use — token mode reuses it for silent re-auth; only the bundle has a 24h staleness window. Rule: reload the unpacked extension fully after changing content/, background/, or confirm/ — Chrome does not hot-reload them.
-
Parameterized queries always:
DB::query('SELECT * FROM %l WHERE id=%i', 'teampass_items', $itemId); // GOOD DB::query("SELECT * FROM teampass_items WHERE id=" . $itemId); // NEVER
-
Session validation on every sensitive operation:
$session = SessionManager::getSession(); if (!$session->has('user-id')) { echo json_encode(['error' => 'Not authenticated']); exit; }
-
CSRF:
owasp/csrf-protector-php— tokens validated on all state-changing requests -
XSS:
voku/anti-xss(server) + DOMPurify (client) +htmlspecialchars() -
Input:
elegantweb/sanitizerfor user input -
Never commit
includes/config/settings.phporTEAMPASS_SECRETS/SECUREFILE -
Never use
exec()/shell_exec()/system()with user input -
Path traversal: validate file paths with
realpath() -
Timing attacks: use
hash_equals()for secret comparisons
PHP Code must pass PHPStan level 4.
declare(strict_types=1)in all new PHP files- Custom classes:
TeampassClasses\*namespace - Constants: defined in
/app/config/include.php
Standard AJAX handler (/sources/*.queries.php):
<?php
declare(strict_types=1);
require_once 'core.php';
$session = SessionManager::getSession();
if (!$session->has('user-id')) { echo json_encode(['error' => 'Not authenticated']); exit; }
$type = filter_input(INPUT_POST, 'type', FILTER_SANITIZE_STRING);
switch ($type) {
case 'action_name':
echo json_encode(['status' => 'success', 'data' => $result]);
break;
default:
echo json_encode(['error' => 'Unknown action']);
}JavaScript (per .eslintrc): single quotes, no semicolons, 2-space indent, const over let, arrow functions, ES6.
Folder tree: always use NestedTree class — never manually update nleft/nright/nlevel.
No PHPUnit test suite. Debugging: PHP error logging, TeamPass Admin > Logs, MeekroDB query hooks, browser console + network tab.
Manual testing checklist: multiple user roles (admin, standard, read-only), personal folders on/off, encryption scenarios, audit log creation, LDAP/OAuth2 if auth changed.
New setting: INSERT into teampass_misc, access via ConfigManager::getSetting(), add to upgrade script, call ConfigManager::invalidateCache() after writes.
New page: /pages/mypage.php + /pages/mypage.js.php + /sources/mypage.queries.php + route in index.php + permission check in sources/core.php.
New API endpoint: controller in /api/Controller/Api/, route in /api/index.php, JWT validation, JSON response.
DB schema change: new /install/upgrade_run_X.X.X.php, update version in /app/config/include.php, test upgrade path.
PR from GitHub:
- Analyse the original issue and the PR changes
- Confirm the fix is appropriate
- Ensure PHPStan level 4 compatibility
index.php— entry point, session validation, page routingsources/core.php— session init and validation (loaded by all backends)sources/main.functions.php— shared utility functions (150KB+)sources/identify.php— login authentication logicincludes/config/include.php— application constantsincludes/config/settings.php— DB credentials (never commit)install/upgrade.php— version detection and upgrade orchestrationapi/index.php— API router with JWT validationscripts/background_tasks___handler.php— cron job entry point
Constants in /app/config/include.php: TP_VERSION (major.minor), TP_VERSION_MINOR (patch). Upgrades via /install/upgrade_run_*.php.
- Always prioritize minimal modifications.
- Never suggest a complete refactor.
- Respect the existing coding style.
- New functions must remain compatible with PHP 8.2.
- Secure all user inputs (SQLi, XSS).
- Do not invent things; rely strictly on factual data.
MySQL default since 5.7.5. Rules:
- Every non-aggregated SELECT column must be in GROUP BY, or functionally dependent on the GROUP BY key
- Never
SELECT *with partial GROUP BY - Prefer
SELECT DISTINCTover GROUP BY when no aggregation needed - Aliases defined in SELECT can be referenced in GROUP BY (MySQL extension, valid)
- Readable code, useful but not excessive comments
- No external libraries without explicit request
- Comments in English
- Commit messages must always be in English
- Use simple and concise sentences
- PR branches must target
pr-XXXXwith XXX = GitHub ID - All public functions must have a docblock
- Variable names in English only
- No
var_dump()orconsole.log()in production - Watch for impacts on
installandupgrade
IMPORTANT: This project has a knowledge graph. ALWAYS use the code-review-graph MCP tools BEFORE using Grep/Glob/Read to explore the codebase. The graph is faster, cheaper (fewer tokens), and gives you structural context (callers, dependents, test coverage) that file scanning cannot.
- Exploring code:
semantic_search_nodesorquery_graphinstead of Grep - Understanding impact:
get_impact_radiusinstead of manually tracing imports - Code review:
detect_changes+get_review_contextinstead of reading entire files - Finding relationships:
query_graphwith callers_of/callees_of/imports_of/tests_for - Architecture questions:
get_architecture_overview+list_communities
Fall back to Grep/Glob/Read only when the graph doesn't cover what you need.
| Tool | Use when |
|---|---|
detect_changes |
Reviewing code changes — gives risk-scored analysis |
get_review_context |
Need source snippets for review — token-efficient |
get_impact_radius |
Understanding blast radius of a change |
get_affected_flows |
Finding which execution paths are impacted |
query_graph |
Tracing callers, callees, imports, tests, dependencies |
semantic_search_nodes |
Finding functions/classes by name or keyword |
get_architecture_overview |
Understanding high-level codebase structure |
refactor_tool |
Planning renames, finding dead code |
- The graph auto-updates on file changes (via hooks).
- Use
detect_changesfor code review. - Use
get_affected_flowsto understand impact. - Use
query_graphpattern="tests_for" to check coverage.