Stats
Actions
Tags
From odoo-claude-code
Runs a git workflow guard before Bash tool execution to enforce policy, checks dependency versions after file edits, and performs a self-check on stop. Executes bash scripts with file access.
3 events · 8 hooks
Safety signals detected in this hook configuration
Where this hook configuration is defined
Defined in hooks/hooks.json
Event handlers and matchers — expand Raw Configuration for the full JSON
tool == "Stop"const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// ===== ODOO PROJECT DETECTION =====
function isOdooProject(startPath) {
if (!startPath) return false;
let currentPath = path.resolve(startPath);
const root = path.parse(currentPath).root;
while (currentPath !== root && currentPath) {
const manifestFiles = ['__manifest__.py', '__openerp__.py'];
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(currentPath, manifest))) return true;
}
const hasModelsDir = fs.existsSync(path.join(currentPath, 'models'));
const hasViewsDir = fs.existsSync(path.join(currentPath, 'views'));
if (hasModelsDir && hasViewsDir) {
const parent = path.dirname(currentPath);
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(parent, manifest))) return true;
}
if (parent.endsWith('addons') || parent.endsWith('odoo')) return true;
}
currentPath = path.dirname(currentPath);
}
return false;
}
const cwd = process.env.GIT_WORKING_DIR || process.cwd();
if (!isOdooProject(cwd)) { process.exit(0); }
// ===== END ODOO PROJECT DETECTION =====
console.log('\x1b[36mℹ️ [Hook] Session ending. Running final checks...\x1b[0m');
// Check for console.log statements
const changedFiles = execSync('git diff --name-only HEAD~1..HEAD', { encoding: 'utf8' });
const pythonFiles = changedFiles.split('\n').filter(f => f.endsWith('.py'));
let hasConsoleLog = false;
for (const file of pythonFiles) {
if (!fs.existsSync(file)) continue;
const content = fs.readFileSync(file, 'utf8');
if (content.includes('print(') || content.includes('console.log(')) {
console.error('\x1b[33m⚠️ [Hook] Found console.log in ' + file + '\x1b[0m');
console.error('Please remove console.log before committing.');
hasConsoleLog = true;
}
}
// Check for TODO/FIXME without tickets
let hasTodo = false;
for (const file of pythonFiles) {
if (!fs.existsSync(file)) continue;
const content = fs.readFileSync(file, 'utf8');
const todoMatches = content.matchAll(/TODO|FIXME/gi);
if (todoMatches.length > 0) {
const hasTicket = content.includes('TODO(') || content.includes('FIXME(');
if (!hasTicket) {
console.error('\x1b[33m⚠️ [Hook] Found TODO/FIXME without ticket reference in ' + file + '\x1b[0m');
console.error('Please add a ticket reference like TODO(#123).');
hasTodo = true;
}
}
}
// Check for hardcoded secrets
const secretPatterns = [/api[_-]?key\s*[=:]\s*["'][a-zA-Z0-9_-]+["']/gi, /password\s*[=:]\s*["'][a-zA-Z0-9_-]+["']/gi];
for (const file of pythonFiles) {
if (!fs.existsSync(file)) continue;
const content = fs.readFileSync(file, 'utf8');
for (const pattern of secretPatterns) {
const matches = content.match(pattern);
if (matches) {
console.error('\x1b[31m🔴 [Hook] Potential hardcoded secret in ' + file + '\x1b[0m');
console.error('Match: ' + matches[0]);
console.error('Never commit secrets! Use environment variables or system parameters.');
}
}
}
if (!hasConsoleLog && !hasTodo) {
console.log('\x1b[32m✓ No console.log or TODO issues found.\x1b[0m');
}
tool == "Write" && tool_input.file_path matches "\\.(py)$"const fs = require('fs');
const path = require('path');
const filePath = process.env.TOOL_INPUT_FILE_PATH || process.env.WRITE_FILE_PATH;
// ===== ODOO PROJECT DETECTION =====
function isOdooProject(startPath) {
if (!startPath) return false;
let currentPath = path.resolve(startPath);
const root = path.parse(currentPath).root;
while (currentPath !== root && currentPath) {
// Check for Odoo-specific files/directories
const manifestFiles = ['__manifest__.py', '__openerp__.py'];
const odooDirs = ['odoo', 'openerp'];
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(currentPath, manifest))) return true;
}
// Check if we're inside an Odoo module directory
const hasModelsDir = fs.existsSync(path.join(currentPath, 'models'));
const hasViewsDir = fs.existsSync(path.join(currentPath, 'views'));
const hasControllersDir = fs.existsSync(path.join(currentPath, 'controllers'));
if (hasModelsDir && (hasViewsDir || hasControllersDir)) {
// Check if parent has manifest or is odoo/addons
const parent = path.dirname(currentPath);
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(parent, manifest))) return true;
}
if (parent.endsWith('addons') || parent.endsWith('odoo')) return true;
}
currentPath = path.dirname(currentPath);
}
return false;
}
if (!isOdooProject(filePath)) {
process.exit(0); // Not an Odoo project, skip hook
}
// ===== END ODOO PROJECT DETECTION =====
// Check if file is in tests/ directory
if (filePath && (filePath.includes('/tests/') || filePath.includes('\\tests\\'))) {
// Test files are OK to write without tests first
process.exit(0);
}
// Check if file is a Python model file
const isModelFile = filePath && filePath.match(/models\/[^\/]+\.py$/);
if (isModelFile) {
// Check if corresponding test file exists
const basePath = path.dirname(filePath);
const testsPath = path.resolve(basePath, '../../tests');
const modelName = path.basename(filePath, '.py');
const testFileName = `test_${modelName}.py`;
const testFilePath = path.resolve(testsPath, testFileName);
if (fs.existsSync(testPath) && !fs.existsSync(testFilePath)) {
console.error('\x1b[33m⚠️ [Hook] No test file found for ' + modelName + '\x1b[0m');
console.error('Expected test at: ' + testFilePath);
console.error('Please write tests first before implementing model (TDD).');
}
}
tool == "Write" && tool_input.file_path matches "\\.(xml)$"const fs = require('fs');
const path = require('path');
const filePath = process.env.TOOL_INPUT_FILE_PATH || process.env.WRITE_FILE_PATH;
// ===== ODOO PROJECT DETECTION =====
function isOdooProject(startPath) {
if (!startPath) return false;
let currentPath = path.resolve(startPath);
const root = path.parse(currentPath).root;
while (currentPath !== root && currentPath) {
const manifestFiles = ['__manifest__.py', '__openerp__.py'];
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(currentPath, manifest))) return true;
}
const hasModelsDir = fs.existsSync(path.join(currentPath, 'models'));
const hasViewsDir = fs.existsSync(path.join(currentPath, 'views'));
if (hasModelsDir && hasViewsDir) {
const parent = path.dirname(currentPath);
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(parent, manifest))) return true;
}
if (parent.endsWith('addons') || parent.endsWith('odoo')) return true;
}
currentPath = path.dirname(currentPath);
}
return false;
}
if (!isOdooProject(filePath)) { process.exit(0); }
// ===== END ODOO PROJECT DETECTION =====
// Check if file is in security/ directory
if (filePath && (filePath.includes('/security/') || filePath.includes('\\security\\'))) {
// Security files require attention
console.log('\x1b[36mℹ️ [Hook] Security file modified: ' + filePath + '\x1b[0m');
console.log('Remember to test access rights and record rules.');
}
tool == "Edit" && tool_input.file_path matches "\\.py$"/* Odoo Deprecated API Detection Hook */
const fs = require('fs');
const path = require('path');
const filePath = process.env.EDIT_FILE_PATH;
const newContent = process.env.EDIT_NEW_CONTENT || '';
const oldContent = process.env.EDIT_OLD_CONTENT || '';
// ===== ODOO PROJECT DETECTION =====
function isOdooProject(startPath) {
if (!startPath) return false;
let currentPath = path.resolve(startPath);
const root = path.parse(currentPath).root;
while (currentPath !== root && currentPath) {
const manifestFiles = ['__manifest__.py', '__openerp__.py'];
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(currentPath, manifest))) return true;
}
const hasModelsDir = fs.existsSync(path.join(currentPath, 'models'));
const hasViewsDir = fs.existsSync(path.join(currentPath, 'views'));
if (hasModelsDir && hasViewsDir) {
const parent = path.dirname(currentPath);
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(parent, manifest))) return true;
}
if (parent.endsWith('addons') || parent.endsWith('odoo')) return true;
}
currentPath = path.dirname(currentPath);
}
return false;
}
if (!isOdooProject(filePath)) { process.exit(0); }
// ===== END ODOO PROJECT DETECTION =====
if (!filePath || !filePath.endsWith('.py')) {
process.exit(0);
}
const warnings = [];
// Pattern 1: _columns = (old Odoo API)
const columnsPattern = /_columns\s*=\s*\{/g;
if (columnsPattern.test(oldContent) || columnsPattern.test(newContent)) {
warnings.push({
severity: 'high',
message: 'Detected deprecated _columns = usage. Use fields module instead.',
pattern: '_columns =',
fix: 'Replace _columns with field definitions using fields.Char, fields.Many2one, etc.'
});
}
// Pattern 2: osv.osv or osv.orm (very old API)
const osvPattern = /osv\.(osv|orm)\(/g;
if (osvPattern.test(oldContent) || osvPattern.test(newContent)) {
warnings.push({
severity: 'critical',
message: 'Detected deprecated osv.osv/osv.orm usage. Use models.Model instead.',
pattern: 'osv.osv or osv.orm',
fix: 'Use class MyModel(models.Model): instead of osv.osv'
});
}
// Pattern 3: @api.one decorator (deprecated since Odoo 16, removed in 17+)
const apiOnePattern = /@api\.one\b/g;
if (apiOnePattern.test(oldContent) || apiOnePattern.test(newContent)) {
warnings.push({
severity: 'critical',
message: 'Detected deprecated @api.one decorator. Removed in Odoo 17+.',
pattern: '@api.one',
fix: 'Remove @api.one and iterate over self directly in your method'
});
}
// Pattern 4: @api.multi decorator (deprecated since Odoo 16)
const apiMultiPattern = /@api\.multi\b/g;
if (apiMultiPattern.test(oldContent) || apiMultiPattern.test(newContent)) {
warnings.push({
severity: 'high',
message: 'Detected deprecated @api.multi decorator. Not needed in Odoo 16+.',
pattern: '@api.multi',
fix: 'Remove @api.multi decorator - methods now operate on recordsets by default'
});
}
// Pattern 5: fields.function (deprecated)
const functionPattern = /fields\.function\(/g;
if (functionPattern.test(oldContent) || functionPattern.test(newContent)) {
warnings.push({
severity: 'high',
message: 'Detected deprecated fields.function. Use fields with compute parameter.',
pattern: 'fields.function',
fix: 'Use fields.Char(compute="_compute_method", store=True) instead'
});
}
// Pattern 6: _defaults dictionary (deprecated)
const defaultsPattern = /_defaults\s*=\s*\{/g;
if (defaultsPattern.test(oldContent) || defaultsPattern.test(newContent)) {
warnings.push({
severity: 'medium',
message: 'Detected deprecated _defaults dictionary. Use default parameter on fields.',
pattern: '_defaults =',
fix: 'Use default=... parameter in field definition instead'
});
}
// Pattern 7: @api.returns with lambda (deprecated in 17+)
const returnsPattern = /@api\.returns\([^,]+,\s*lambda/g;
if (returnsPattern.test(oldContent) || returnsPattern.test(newContent)) {
warnings.push({
severity: 'medium',
message: 'Detected deprecated @api.returns with lambda. Use simpler form.',
pattern: '@api.returns(self, lambda ...)',
fix: 'Use @api.returns("self") without lambda parameter'
});
}
// Output warnings
if (warnings.length > 0) {
console.log('\x1b[33m=== Odoo Deprecated API Detection ===\x1b[0m');
warnings.forEach((w, i) => {
const severity = w.severity.toUpperCase();
const emoji = { 'critical': '🚨', 'high': '⚠️', 'medium': '⚠', 'low': 'ℹ' };
console.log(`${emoji[w.severity] || ''} [${severity}] ${w.message}`);
console.log(` Pattern: ${w.pattern}`);
console.log(` Fix: ${w.fix}`);
});
console.log('\x1b[33m======================================\x1b[0m\n');
}
process.exit(0);
tool == "Edit" && tool_input.file_path matches "\\.py$"/* Odoo Performance Pattern Detection Hook */
const fs = require('fs');
const path = require('path');
const filePath = process.env.EDIT_FILE_PATH;
const newContent = process.env.EDIT_NEW_CONTENT || '';
const oldContent = process.env.EDIT_OLD_CONTENT || '';
// ===== ODOO PROJECT DETECTION =====
function isOdooProject(startPath) {
if (!startPath) return false;
let currentPath = path.resolve(startPath);
const root = path.parse(currentPath).root;
while (currentPath !== root && currentPath) {
const manifestFiles = ['__manifest__.py', '__openerp__.py'];
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(currentPath, manifest))) return true;
}
const hasModelsDir = fs.existsSync(path.join(currentPath, 'models'));
const hasViewsDir = fs.existsSync(path.join(currentPath, 'views'));
if (hasModelsDir && hasViewsDir) {
const parent = path.dirname(currentPath);
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(parent, manifest))) return true;
}
if (parent.endsWith('addons') || parent.endsWith('odoo')) return true;
}
currentPath = path.dirname(currentPath);
}
return false;
}
if (!isOdooProject(filePath)) { process.exit(0); }
// ===== END ODOO PROJECT DETECTION =====
if (!filePath || !filePath.endsWith('.py')) {
process.exit(0);
}
const warnings = [];
// Pattern 1: search() inside loop (N+1 query problem)
const loopSearchPattern = /for\s+\w+\s+in\s+[^:]+:(?!.*#.*skip|.*#.*ignore)[\s\S]*\.search\(/gm;
if (loopSearchPattern.test(oldContent) || loopSearchPattern.test(newContent)) {
warnings.push({
severity: 'high',
message: 'Potential N+1 query problem: search() inside loop.',
pattern: 'for ... in ...: ...search()',
fix: 'Use search() before loop or use search_read() with prefetch'
});
}
// Pattern 2: Computed field without store=True when used in search
const computedNoStorePattern = /fields\.(\w+)\s*\([^)]*compute[^)]*\)(?!.*store\s*=\s*True)/g;
if (computedNoStorePattern.test(newContent) && newContent.includes('search([')) {
warnings.push({
severity: 'medium',
message: 'Computed field used in search without store=True.',
pattern: 'computed field without store=True',
fix: 'Add store=True to computed field for filtering/searching'
});
}
// Pattern 3: Many2one without index
const many2oneNoIndexPattern = /fields\.Many2one\([^)]*\)(?!.*index\s*=\s*True)/g;
if (many2oneNoIndexPattern.test(newContent)) {
warnings.push({
severity: 'low',
message: 'Many2one field without index=True.',
pattern: 'fields.Many2one without index',
fix: 'Add index=True to Many2one fields used in searches'
});
}
// Pattern 4: Selection field used in filter without index
const selectionSearchPattern = /state\s*in\s*\[/g;
if (selectionSearchPattern.test(newContent)) {
warnings.push({
severity: 'low',
message: 'Selection field used in filter. Consider adding index.',
pattern: 'state field filtering',
fix: 'Add index=True to selection fields used in domains'
});
}
// Pattern 5: write() called on individual records in loop
const loopWritePattern = /for\s+\w+\s+in\s+[^:]+:(?!.*#.*bulk|.*#.*batch)[\s\S]*\.write\(/gm;
if (loopWritePattern.test(oldContent) || loopWritePattern.test(newContent)) {
warnings.push({
severity: 'medium',
message: 'write() called in loop. Consider bulk write.',
pattern: 'for ... in ...: ...write()',
fix: 'Use records.write({...}) for bulk operations'
});
}
// Pattern 6: create() called in loop
const loopCreatePattern = /for\s+\w+\s+in\s+[^:]+:(?!.*#.*bulk)[\s\S]*\.create\(/gm;
if (loopCreatePattern.test(oldContent) || loopCreatePattern.test(newContent)) {
warnings.push({
severity: 'medium',
message: 'create() called in loop. Consider bulk create.',
pattern: 'for ... in ...: ...create()',
fix: 'Use Model.create([...]) for bulk operations'
});
}
// Output warnings
if (warnings.length > 0) {
console.log('\x1b[33m=== Odoo Performance Pattern Detection ===\x1b[0m');
warnings.forEach((w, i) => {
const severity = w.severity.toUpperCase();
const emoji = { 'critical': '🚨', 'high': '⚠️', 'medium': '⚠', 'low': 'ℹ' };
console.log(`${emoji[w.severity] || ''} [${severity}] ${w.message}`);
console.log(` Pattern: ${w.pattern}`);
console.log(` Fix: ${w.fix}`);
});
console.log('\x1b[33m=========================================\x1b[0m\n');
}
process.exit(0);
tool == "Edit" && tool_input.file_path matches "\\.js$"/* Odoo Widget System Deprecation Detection */
const fs = require('fs');
const path = require('path');
const filePath = process.env.EDIT_FILE_PATH;
const newContent = process.env.EDIT_NEW_CONTENT || '';
const oldContent = process.env.EDIT_OLD_CONTENT || '';
// ===== ODOO PROJECT DETECTION =====
function isOdooProject(startPath) {
if (!startPath) return false;
let currentPath = path.resolve(startPath);
const root = path.parse(currentPath).root;
while (currentPath !== root && currentPath) {
const manifestFiles = ['__manifest__.py', '__openerp__.py'];
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(currentPath, manifest))) return true;
}
const hasModelsDir = fs.existsSync(path.join(currentPath, 'models'));
const hasViewsDir = fs.existsSync(path.join(currentPath, 'views'));
if (hasModelsDir && hasViewsDir) {
const parent = path.dirname(currentPath);
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(parent, manifest))) return true;
}
if (parent.endsWith('addons') || parent.endsWith('odoo')) return true;
}
// Also check for static/src/js which indicates Odoo web module
if (fs.existsSync(path.join(currentPath, 'static', 'src', 'js'))) return true;
currentPath = path.dirname(currentPath);
}
return false;
}
if (!isOdooProject(filePath)) { process.exit(0); }
// ===== END ODOO PROJECT DETECTION =====
if (!filePath || !filePath.endsWith('.js')) {
process.exit(0);
}
const warnings = [];
// Pattern 1: Widget.extend (deprecated in Odoo 19, use OWL)
const widgetExtendPattern = /Widget\.extend\s*\(/g;
if (widgetExtendPattern.test(oldContent) || widgetExtendPattern.test(newContent)) {
warnings.push({
severity: 'high',
message: 'Widget.extend is deprecated. Use OWL components in Odoo 19.',
pattern: 'Widget.extend',
fix: 'Use OWL Component class with setup() instead of Widget.extend'
});
}
// Pattern 2: var Widget = require('web.Widget')
const widgetRequirePattern = /var\s+Widget\s*=\s*require\(['"]web\.Widget['"])/g;
if (widgetRequirePattern.test(oldContent) || widgetRequirePattern.test(newContent)) {
warnings.push({
severity: 'high',
message: 'web.Widget is deprecated. Use OWL in Odoo 19.',
pattern: 'require("web.Widget")',
fix: 'Use OWL Component and require("owl") instead'
});
}
// Pattern 3: this.$() jQuery usage (OWL uses refs)
const jqueryPattern = /this\.\$\(/g;
if (jqueryPattern.test(newContent)) {
warnings.push({
severity: 'medium',
message: 'jQuery this.$() found. Use OWL refs instead.',
pattern: 'this.$()',
fix: 'Use owl.hooks.useRef() for direct DOM access in OWL'
});
}
// Output warnings
if (warnings.length > 0) {
console.log('\x1b[33m=== Odoo Frontend Deprecation Detection ===\x1b[0m');
warnings.forEach(w => {
const severity = w.severity.toUpperCase();
console.log(`\u26a0 [${severity}] ${w.message}`);
console.log(` Pattern: ${w.pattern}`);
console.log(` Fix: ${w.fix}`);
});
console.log('\x1b[33m========================================\x1b[0m\n');
}
process.exit(0);
tool == "Bash" && tool_input.command matches "git commit"const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// ===== ODOO PROJECT DETECTION =====
function isOdooProject(startPath) {
if (!startPath) return false;
let currentPath = path.resolve(startPath);
const root = path.parse(currentPath).root;
while (currentPath !== root && currentPath) {
const manifestFiles = ['__manifest__.py', '__openerp__.py'];
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(currentPath, manifest))) return true;
}
const hasModelsDir = fs.existsSync(path.join(currentPath, 'models'));
const hasViewsDir = fs.existsSync(path.join(currentPath, 'views'));
if (hasModelsDir && hasViewsDir) {
const parent = path.dirname(currentPath);
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(parent, manifest))) return true;
}
if (parent.endsWith('addons') || parent.endsWith('odoo')) return true;
}
currentPath = path.dirname(currentPath);
}
return false;
}
const cwd = process.env.GIT_WORKING_DIR || process.cwd();
if (!isOdooProject(cwd)) { process.exit(0); }
// ===== END ODOO PROJECT DETECTION =====
try {
// Run pylint on modified Python files
const changedFiles = execSync('git diff --name-only HEAD~1..HEAD', { encoding: 'utf8' });
const pythonFiles = changedFiles.split('\n').filter(f => f.endsWith('.py'));
if (pythonFiles.length > 0) {
console.log('\x1b[36mℹ️ [Hook] Running PEP8 check...\x1b[0m');
try {
execSync('pylint --py3k ' + pythonFiles.join(' '), { stdio: 'inherit' });
console.log('\x1b[32m✓ PEP8 check passed\x1b[0m');
} catch (e) {
console.error('\x1b[33m⚠️ PEP8 issues detected. Please fix before committing.\x1b[0m');
}
}
} catch (e) {
// Pylint may not be available, that's OK
console.log('PEP8 check skipped (pylint not available)');
}tool == "Edit" && tool_input.file_path matches "\\.(py)$"const fs = require('fs');
const path = require('path');
const filePath = process.env.EDIT_FILE_PATH;
// ===== ODOO PROJECT DETECTION =====
function isOdooProject(startPath) {
if (!startPath) return false;
let currentPath = path.resolve(startPath);
const root = path.parse(currentPath).root;
while (currentPath !== root && currentPath) {
const manifestFiles = ['__manifest__.py', '__openerp__.py'];
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(currentPath, manifest))) return true;
}
const hasModelsDir = fs.existsSync(path.join(currentPath, 'models'));
const hasViewsDir = fs.existsSync(path.join(currentPath, 'views'));
if (hasModelsDir && hasViewsDir) {
const parent = path.dirname(currentPath);
for (const manifest of manifestFiles) {
if (fs.existsSync(path.join(parent, manifest))) return true;
}
if (parent.endsWith('addons') || parent.endsWith('odoo')) return true;
}
currentPath = path.dirname(currentPath);
}
return false;
}
if (!isOdooProject(filePath)) { process.exit(0); }
// ===== END ODOO PROJECT DETECTION =====
console.log('\x1b[36mℹ️ [Hook] Modified Python file: ' + path.basename(filePath) + '\x1b[0m');
console.log('Remember to run tests for this file.');npx claudepluginhub echozen88/odoo-claude-code --plugin odoo-claude-code