fix: proper project isolation and relative path matching for file-context hook

- Use getProjectContext(cwd).allProjects for project scoping (same as SessionStart)
- Convert absolute file_path to relative using cwd (observations store relative paths)
- API accepts comma-separated projects param with IN() SQL filter
- Remove basename matching — use full relative path to avoid cross-file collisions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-03-18 15:38:53 -07:00
parent 1d48f63b99
commit e07b13f7de
4 changed files with 109 additions and 105 deletions
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -9,9 +9,11 @@ import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js'
import { ensureWorkerRunning, workerHttpRequest } from '../../shared/worker-utils.js'; import { ensureWorkerRunning, workerHttpRequest } from '../../shared/worker-utils.js';
import { logger } from '../../utils/logger.js'; import { logger } from '../../utils/logger.js';
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js'; import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
import path from 'path';
import { isProjectExcluded } from '../../utils/project-filter.js'; import { isProjectExcluded } from '../../utils/project-filter.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js'; import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js'; import { USER_SETTINGS_PATH } from '../../shared/paths.js';
import { getProjectContext } from '../../utils/project-name.js';
const TYPE_ICONS: Record<string, string> = { const TYPE_ICONS: Record<string, string> = {
decision: '\u2696\uFE0F', decision: '\u2696\uFE0F',
@@ -103,7 +105,13 @@ export const fileContextHandler: EventHandler = {
// Query worker for observations related to this file // Query worker for observations related to this file
try { try {
const queryParams = new URLSearchParams({ path: filePath }); const context = getProjectContext(input.cwd);
// Observations store relative paths — convert absolute to relative using cwd
const cwd = input.cwd || process.cwd();
const relativePath = path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath;
const queryParams = new URLSearchParams({ path: relativePath });
// Pass all project names (parent + worktree) for unified lookup
queryParams.set('projects', context.allProjects.join(','));
const response = await workerHttpRequest(`/api/observations/by-file?${queryParams.toString()}`, { const response = await workerHttpRequest(`/api/observations/by-file?${queryParams.toString()}`, {
method: 'GET', method: 'GET',
+10 -15
View File
@@ -4,7 +4,6 @@
*/ */
import { Database } from 'bun:sqlite'; import { Database } from 'bun:sqlite';
import path from 'path';
import { logger } from '../../../utils/logger.js'; import { logger } from '../../../utils/logger.js';
import type { ObservationRecord } from '../../../types/database.js'; import type { ObservationRecord } from '../../../types/database.js';
import type { GetObservationsByIdsOptions, ObservationSessionRow } from './types.js'; import type { GetObservationsByIdsOptions, ObservationSessionRow } from './types.js';
@@ -114,29 +113,25 @@ export function getObservationsForSession(
} }
/** /**
* Get observations associated with a given file path. * Get observations associated with a given file path, scoped to specific projects.
* Searches both files_read and files_modified using basename matching * Matches on the full file path (not just basename) to avoid cross-project collisions.
* to handle differing absolute paths across sessions.
*/ */
export function getObservationsByFilePath( export function getObservationsByFilePath(
db: Database, db: Database,
filePath: string, filePath: string,
options?: { project?: string; limit?: number } options?: { projects?: string[]; limit?: number }
): ObservationRecord[] { ): ObservationRecord[] {
const basename = path.basename(filePath); const likePattern = `%${filePath}%`;
const likePattern = `%${basename}%`;
const limit = options?.limit ?? 30; const limit = options?.limit ?? 30;
const additionalConditions: string[] = [];
const params: any[] = [likePattern, likePattern]; const params: any[] = [likePattern, likePattern];
if (options?.project) { let projectClause = '';
additionalConditions.push('AND project = ?'); if (options?.projects?.length) {
params.push(options.project); const placeholders = options.projects.map(() => '?').join(',');
projectClause = `AND project IN (${placeholders})`;
params.push(...options.projects);
} }
const additionalWhere = additionalConditions.join(' ');
const stmt = db.prepare(` const stmt = db.prepare(`
SELECT * SELECT *
FROM observations FROM observations
@@ -144,7 +139,7 @@ export function getObservationsByFilePath(
EXISTS (SELECT 1 FROM json_each(files_read) WHERE value LIKE ?) EXISTS (SELECT 1 FROM json_each(files_read) WHERE value LIKE ?)
OR EXISTS (SELECT 1 FROM json_each(files_modified) WHERE value LIKE ?) OR EXISTS (SELECT 1 FROM json_each(files_modified) WHERE value LIKE ?)
) )
${additionalWhere} ${projectClause}
ORDER BY created_at_epoch DESC ORDER BY created_at_epoch DESC
LIMIT ${limit} LIMIT ${limit}
`); `);
@@ -111,8 +111,8 @@ export class DataRoutes extends BaseRouteHandler {
}); });
/** /**
* Get observations associated with a file path * Get observations associated with a file path, scoped to projects
* GET /api/observations/by-file?path=<file_path>&project=<project>&limit=30 * GET /api/observations/by-file?path=<file_path>&projects=<comma,separated>&limit=30
*/ */
private handleGetObservationsByFile = this.wrapHandler((req: Request, res: Response): void => { private handleGetObservationsByFile = this.wrapHandler((req: Request, res: Response): void => {
const filePath = req.query.path as string | undefined; const filePath = req.query.path as string | undefined;
@@ -121,11 +121,12 @@ export class DataRoutes extends BaseRouteHandler {
return; return;
} }
const project = req.query.project as string | undefined; const projectsParam = req.query.projects as string | undefined;
const projects = projectsParam ? projectsParam.split(',').filter(Boolean) : undefined;
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined; const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined;
const db = this.dbManager.getSessionStore().db; const db = this.dbManager.getSessionStore().db;
const observations = getObservationsByFilePath(db, filePath, { project, limit }); const observations = getObservationsByFilePath(db, filePath, { projects, limit });
res.json({ observations, count: observations.length }); res.json({ observations, count: observations.length });
}); });