Skip to content

Commit 1e145bf

Browse files
authored
fix(cursor): deliver user-scope rules to workspaces outside the home directory (#1197) (#1198)
* test(cursor): user-scope rules in a workspace outside the home directory (#1197) * fix(cursor): deliver user-scope rules to workspaces outside the home directory (#1197) Cursor loads ~/.cursor/rules only by walking up from the workspace, so a workspace outside the home directory never got the user-scope rules while doctor reported them delivered. The sessionStart hook now adds them there, and doctor checks that hook. * fix(cursor): keep the merged hook reply under Cursor's limit and read Windows workspace roots Cursor caps a hook reply at 10,000 characters and drops all of it when over, so the dispatcher now leaves the user-scope rules out when the merged reply would not fit, keeping the other session-start hints. The Cursor IDE sends each workspace root as a URI path (/c:/...), which no path API resolves on Windows; resolveHookCwd now drops the slash. Doctor names the real next step for invalid JSON, a team-disabled session-start built-in, and a dispatch for another tool. * fix(cursor): leave headroom for session hints and name the real cause in doctor Doctor now fails past 9,000 characters, where the session's own hints can push the rules out of Cursor's 10,000-character reply. The dispatcher leaves the rules out only when the rest then fits. Doctor reads the --tool value whole and orders its causes: invalid JSON, a missing hook (naming a team builtin.disabled entry), then length. * fix(cursor): check the team env hint against Cursor's limit instead of a fixed headroom The rules check fails only past Cursor's real 10,000-character limit. An informational check measures the rules together with the team env hint, which shares every session's reply, using the hint handler itself, so it flags exactly the case where the dispatcher leaves the rules out. * fix(hooks): log when a Cursor reply stays over the limit without the rules
1 parent 3281eda commit 1e145bf

12 files changed

Lines changed: 478 additions & 9 deletions

‎docs/guide/team-culture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ They are not written into the project's `AGENTS.md`. Each tool gets its own file
3131
|---|---|---|
3232
| Claude Code | `~/.claude/CLAUDE.md` | `.claude/rules/teamai-context.md` |
3333
| Codex family | `$CODEX_HOME/AGENTS.md` | session-start hook |
34-
| Cursor | `~/.cursor/rules/teamai-context.mdc` | `.cursor/rules/teamai-context.mdc` |
34+
| Cursor | `~/.cursor/rules/teamai-context.mdc`; in a workspace outside the home directory, the session-start hook | `.cursor/rules/teamai-context.mdc` |
3535
| CodeBuddy, WorkBuddy | each tool's user file | one shared `.codebuddy/rules/teamai-context.md` |
3636
| OpenCode | `~/.config/opencode/teamai-context.md` | `.opencode/teamai-context.md` |
3737
| Pi, Oh My Pi | each tool's user file | an extension adds them to the system prompt |

‎docs/guide/zh-CN/team-culture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ team:
3131
|---|---|---|
3232
| Claude Code | `~/.claude/CLAUDE.md` | `.claude/rules/teamai-context.md` |
3333
| Codex 系列 | `$CODEX_HOME/AGENTS.md` | session-start hook |
34-
| Cursor | `~/.cursor/rules/teamai-context.mdc` | `.cursor/rules/teamai-context.mdc` |
34+
| Cursor | `~/.cursor/rules/teamai-context.mdc`;工作区不在主目录下时由 session-start hook 加入 | `.cursor/rules/teamai-context.mdc` |
3535
| CodeBuddy、WorkBuddy | 各自的用户文件 | 共用 `.codebuddy/rules/teamai-context.md` |
3636
| OpenCode | `~/.config/opencode/teamai-context.md` | `.opencode/teamai-context.md` |
3737
| Pi、Oh My Pi | 各自的用户文件 | 扩展加进系统提示 |
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import path from 'node:path';
3+
import os from 'node:os';
4+
import fse from 'fs-extra';
5+
6+
import { buildHandlerRegistry } from '../hook-handlers.js';
7+
import type { LocalConfig } from '../types.js';
8+
9+
describe('Cursor gets the user-scope rules from its session-start hook in a workspace outside the home directory (#1197)', () => {
10+
let tmpDir: string;
11+
let home: string;
12+
let userRepo: string;
13+
let userConfig: LocalConfig;
14+
15+
const context = async (cwd: string, tool = 'cursor', config: LocalConfig | null = userConfig): Promise<string | null> => {
16+
const registration = buildHandlerRegistry().find((r) => r.event === 'session-start' && r.handler.name === 'cursor-user-rules');
17+
const output = await registration!.handler.execute({ hook_event_name: 'SessionStart', cwd }, tool, config);
18+
if (output === null) return null;
19+
const parsed = JSON.parse(output) as { hookSpecificOutput: { hookEventName: string; additionalContext: string } };
20+
expect(parsed.hookSpecificOutput.hookEventName).toBe('SessionStart');
21+
return parsed.hookSpecificOutput.additionalContext;
22+
};
23+
24+
beforeEach(async () => {
25+
tmpDir = await fse.realpath(await fse.mkdtemp(path.join(os.tmpdir(), 'teamai-cursor-user-rules-')));
26+
home = path.join(tmpDir, 'home');
27+
vi.stubEnv('HOME', home);
28+
vi.stubEnv('USERPROFILE', home);
29+
userRepo = path.join(tmpDir, 'user-team-repo');
30+
await fse.outputFile(path.join(userRepo, 'teamai.yaml'), 'team: test\nrepo: https://example.invalid/x/team.git\n');
31+
await fse.outputFile(path.join(userRepo, 'rules', 'codeword.md'), 'The team codeword is PELICAN-42.\n');
32+
await fse.outputFile(path.join(userRepo, 'rules', 'scoped.md'), '---\npaths:\n - "src/**"\n---\nPrefer named exports.\n');
33+
await fse.outputFile(
34+
path.join(home, '.teamai', 'config.yaml'),
35+
`repo:\n localPath: ${userRepo}\n remote: https://example.invalid/x/team.git\nusername: u\n`,
36+
);
37+
await fse.ensureDir(path.join(home, '.cursor', 'rules'));
38+
userConfig = {
39+
repo: { localPath: userRepo, remote: 'https://example.invalid/x/team.git' },
40+
username: 'u',
41+
additionalRoles: [],
42+
scope: 'user',
43+
} as unknown as LocalConfig;
44+
});
45+
46+
afterEach(async () => {
47+
vi.unstubAllEnvs();
48+
await fse.remove(tmpDir);
49+
});
50+
51+
it('adds the user rules when the workspace is outside the home directory, a path-scoped rule after its globs', async () => {
52+
const workspace = path.join(tmpDir, 'work', 'project');
53+
await fse.ensureDir(workspace);
54+
55+
const text = await context(workspace);
56+
57+
expect(text).toContain('The team codeword is PELICAN-42.');
58+
expect(text).toContain('Applies to files matching: src/**\nPrefer named exports.');
59+
expect(text).not.toContain('paths:');
60+
});
61+
62+
it('adds the user-scope culture and shared instructions, which ~/.cursor/rules also holds', async () => {
63+
await fse.outputFile(path.join(userRepo, 'culture.md'), '---\ncompany:\n name: Acme\n---\n\nBe kind to teammates.\n');
64+
await fse.outputFile(path.join(userRepo, 'claudemd', 'shared.md'), 'Shared team instructions.\n');
65+
66+
const text = (await context(path.join(tmpDir, 'work')))!;
67+
68+
expect(text).toContain('Be kind to teammates.');
69+
expect(text).toContain('Shared team instructions.');
70+
expect(text).toContain('PELICAN-42');
71+
expect(text).not.toContain('<!--');
72+
});
73+
74+
it('adds nothing under the home directory, where Cursor loads ~/.cursor/rules itself', async () => {
75+
const workspace = path.join(home, 'src', 'project');
76+
await fse.ensureDir(workspace);
77+
78+
expect(await context(workspace)).toBeNull();
79+
});
80+
81+
it('adds nothing when a link on the walk already points at ~/.cursor/rules', async () => {
82+
const workspace = path.join(tmpDir, 'work', 'project');
83+
await fse.ensureDir(path.join(tmpDir, 'work', '.cursor'));
84+
await fse.symlink(path.join(home, '.cursor', 'rules'), path.join(tmpDir, 'work', '.cursor', 'rules'), 'junction');
85+
await fse.ensureDir(workspace);
86+
87+
expect(await context(workspace)).toBeNull();
88+
});
89+
90+
it('adds the user rules, not the project\'s, in a teamai project outside the home directory', async () => {
91+
const projectRepo = path.join(tmpDir, 'project-team-repo');
92+
await fse.outputFile(path.join(projectRepo, 'teamai.yaml'), 'team: proj\nrepo: https://example.invalid/x/proj.git\n');
93+
await fse.outputFile(path.join(projectRepo, 'rules', 'project.md'), 'The project codeword is HERON-7.\n');
94+
const projectRoot = path.join(tmpDir, 'work', 'project');
95+
await fse.ensureDir(projectRoot);
96+
const projectConfig = {
97+
repo: { localPath: projectRepo, remote: 'https://example.invalid/x/proj.git' },
98+
username: 'u',
99+
additionalRoles: [],
100+
scope: 'project',
101+
projectRoot,
102+
} as unknown as LocalConfig;
103+
104+
const text = await context(projectRoot, 'cursor', projectConfig);
105+
106+
expect(text).toContain('PELICAN-42');
107+
expect(text).not.toContain('HERON-7');
108+
});
109+
110+
it('adds nothing for a tool other than Cursor, or when Cursor is excluded from the user scope', async () => {
111+
const workspace = path.join(tmpDir, 'work');
112+
await fse.ensureDir(workspace);
113+
114+
expect(await context(workspace, 'claude')).toBeNull();
115+
expect(await context(workspace, 'cursor', { ...userConfig, disabledAgents: ['cursor'] } as LocalConfig)).toBeNull();
116+
});
117+
118+
it('adds nothing when the text is over Cursor\'s limit, which would drop the whole hook reply', async () => {
119+
await fse.outputFile(path.join(userRepo, 'rules', 'long.md'), `${'x'.repeat(10_000)}\n`);
120+
const workspace = path.join(tmpDir, 'work');
121+
await fse.ensureDir(workspace);
122+
123+
expect(await context(workspace)).toBeNull();
124+
});
125+
});

‎src/__tests__/doctor-rules-delivery.test.ts‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,107 @@ describe('doctor — rules delivered on disk', () => {
588588
expect(await namedCheck('Team rules are inlined in Hermes SOUL.md')).toBeUndefined();
589589
});
590590

591+
describe('Cursor workspaces outside the home directory (#1197)', () => {
592+
const CURSOR_HOOKS = '.cursor/hooks.json';
593+
const OUTSIDE_HOME = 'Team rules reach Cursor workspaces outside the home directory';
594+
const WITH_ENV_HINT = 'Team rules and the team env hint fit one Cursor hook reply';
595+
596+
async function writeCursorHooks(sessionStart: unknown[]): Promise<void> {
597+
await fse.outputFile(path.join(homeDir, CURSOR_HOOKS), JSON.stringify({ version: 1, hooks: { sessionStart } }));
598+
}
599+
600+
beforeEach(() => {
601+
teamConfig.toolPaths.cursor = { rules: CURSOR_RULES, settings: CURSOR_HOOKS };
602+
});
603+
604+
it('passes when ~/.cursor/hooks.json registers the teamai sessionStart hook', async () => {
605+
await writeCursorHooks([{ command: 'bash -lc "teamai hook-dispatch session-start --tool cursor 2>/dev/null" || true' }]);
606+
607+
expect(await (await namedCheck(OUTSIDE_HOME))!.check()).toBe(true);
608+
});
609+
610+
it('fails when no teamai sessionStart hook is registered, saying why the rules file is not enough', async () => {
611+
await writeCursorHooks([]);
612+
613+
const check = (await namedCheck(OUTSIDE_HOME))!;
614+
expect(await check.check()).toBe(false);
615+
expect(check.fix).toContain('only in a workspace under the home directory');
616+
expect(check.fix).toContain('teamai hooks inject');
617+
});
618+
619+
it.each([
620+
['teamai hook-dispatch session-start --tool claude', false],
621+
['teamai hook-dispatch session-start --tool cursor-agent', false],
622+
['teamai hook-dispatch session-start --tool=cursor', true],
623+
])('reads the dispatch\'s --tool value: %s', async (command, passes) => {
624+
await writeCursorHooks([{ command }]);
625+
626+
expect(await (await namedCheck(OUTSIDE_HOME))!.check()).toBe(passes);
627+
});
628+
629+
it('passes near the limit when nothing else shares the hook reply', async () => {
630+
await fse.writeFile(path.join(repoPath, 'rules', 'long.md'), `${'x'.repeat(9_500)}\n`);
631+
await writeCursorHooks([{ command: 'teamai hook-dispatch session-start --tool cursor' }]);
632+
633+
expect(await (await namedCheck(OUTSIDE_HOME))!.check()).toBe(true);
634+
expect(await (await namedCheck(WITH_ENV_HINT))!.check()).toBe(true);
635+
});
636+
637+
it('flags the team env hint pushing the rules out of the shared hook reply', async () => {
638+
await fse.writeFile(path.join(repoPath, 'rules', 'long.md'), `${'x'.repeat(9_800)}\n`);
639+
await fse.outputFile(path.join(repoPath, 'env', 'env.yaml'), 'variables:\n - key: API_URL\n value: https://example.invalid\n');
640+
await writeCursorHooks([{ command: 'teamai hook-dispatch session-start --tool cursor' }]);
641+
642+
expect(await (await namedCheck(OUTSIDE_HOME))!.check()).toBe(true);
643+
const shared = (await namedCheck(WITH_ENV_HINT))!;
644+
expect(shared.informational).toBe(true);
645+
expect(await shared.check()).toBe(false);
646+
expect(shared.fix).toContain('leaves the rules out');
647+
});
648+
649+
it('names the length, not the team\'s builtin.disabled entry, while the hook is still registered', async () => {
650+
await fse.outputFile(path.join(repoPath, 'hooks', 'hooks.yaml'), 'builtin:\n disabled:\n - Hook dispatch session-start\n');
651+
await fse.writeFile(path.join(repoPath, 'rules', 'long.md'), `${'x'.repeat(10_000)}\n`);
652+
await writeCursorHooks([{ command: 'teamai hook-dispatch session-start --tool cursor' }]);
653+
654+
const check = (await namedCheck(OUTSIDE_HOME))!;
655+
expect(check.fix).not.toContain('builtin.disabled');
656+
expect(check.fix).toContain('characters');
657+
});
658+
659+
it('tells the member to repair invalid JSON by hand, which hooks inject will not rewrite', async () => {
660+
await fse.outputFile(path.join(homeDir, CURSOR_HOOKS), '{ invalid JSON');
661+
662+
const check = (await namedCheck(OUTSIDE_HOME))!;
663+
expect(await check.check()).toBe(false);
664+
expect(check.fix).toContain('Fix the file by hand');
665+
});
666+
667+
it('names the team\'s builtin.disabled entry when the team turned the session-start hook off', async () => {
668+
await fse.outputFile(path.join(repoPath, 'hooks', 'hooks.yaml'), 'builtin:\n disabled:\n - Hook dispatch session-start\n');
669+
await writeCursorHooks([]);
670+
671+
const check = (await namedCheck(OUTSIDE_HOME))!;
672+
expect(await check.check()).toBe(false);
673+
expect(check.fix).toContain('builtin.disabled');
674+
});
675+
676+
it('fails when the rules are over Cursor\'s hook context limit', async () => {
677+
await fse.writeFile(path.join(repoPath, 'rules', 'long.md'), `${'x'.repeat(10_000)}\n`);
678+
await writeCursorHooks([{ command: 'teamai hook-dispatch session-start --tool cursor' }]);
679+
680+
const check = (await namedCheck(OUTSIDE_HOME))!;
681+
expect(await check.check()).toBe(false);
682+
expect(check.fix).toContain('10000-character limit');
683+
});
684+
685+
it('emits no check in project scope, where the project delivers into the workspace itself', async () => {
686+
Object.assign(localConfig, { scope: 'project', projectRoot: path.join(tempDir, 'project') });
687+
688+
expect(await namedCheck(OUTSIDE_HOME)).toBeUndefined();
689+
});
690+
});
691+
591692
describe('Hermes in project scope (#946)', () => {
592693
beforeEach(async () => {
593694
const hermesHome = path.join(tempDir, 'hermes');

‎src/__tests__/hook-cwd.test.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ describe('resolveHookCwd', () => {
2323
})).toBe('/project');
2424
});
2525

26+
it('turns a Windows workspace root sent as a URI path (/c:/...) into a drive path', () => {
27+
// The Cursor IDE sends `folders.map((f) => f.uri.path)`, which keeps the URI's leading slash.
28+
expect(resolveHookCwd({ workspace_roots: ['/c:/Users/me/project'] })).toBe('c:/Users/me/project');
29+
expect(resolveHookCwd({ cwd: '/D:/work' })).toBe('D:/work');
30+
});
31+
2632
it('skips blank workspace_roots entries', () => {
2733
expect(resolveHookCwd({
2834
workspace_roots: ['', ' ', '/real-root'],

‎src/__tests__/hook-dispatch.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,46 @@ describe('hook-dispatch', () => {
185185
expect(result.output).toBeNull();
186186
});
187187

188+
it('drops a yielding handler\'s context when the merged reply is over Cursor\'s limit, keeping the rest (#1197)', async () => {
189+
const context = (text: string) => JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: text } });
190+
const rules = createHandler('cursor-user-rules', context('R'.repeat(9_990)));
191+
const env = createHandler('team-env-hint', context('Team env variables in this scope: API_URL.'));
192+
const dispatcher = createDispatcher({ localConfig: null, handlers: [
193+
{ event: 'session-start', matcher: '*', handler: rules, yieldsToContextLimit: true },
194+
{ event: 'session-start', matcher: '*', handler: env },
195+
] });
196+
197+
const overCursor = await dispatcher.dispatch('session-start', '*', {}, 'cursor');
198+
const claude = await dispatcher.dispatch('session-start', '*', {}, 'claude');
199+
200+
expect(JSON.parse(overCursor.output!).hookSpecificOutput.additionalContext).toBe('Team env variables in this scope: API_URL.');
201+
expect(JSON.parse(claude.output!).hookSpecificOutput.additionalContext).toContain('R'.repeat(9_990));
202+
});
203+
204+
it('keeps a yielding handler\'s context when the others alone are still over Cursor\'s limit', async () => {
205+
const context = (text: string) => JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: text } });
206+
const dispatcher = createDispatcher({ localConfig: null, handlers: [
207+
{ event: 'session-start', matcher: '*', handler: createHandler('cursor-user-rules', context('RULES')), yieldsToContextLimit: true },
208+
{ event: 'session-start', matcher: '*', handler: createHandler('mr-hint', context('M'.repeat(10_200))) },
209+
] });
210+
211+
const result = await dispatcher.dispatch('session-start', '*', {}, 'cursor');
212+
213+
expect(JSON.parse(result.output!).hookSpecificOutput.additionalContext).toContain('RULES');
214+
});
215+
216+
it('keeps a yielding handler\'s context when the merged reply fits Cursor\'s limit', async () => {
217+
const context = (text: string) => JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: text } });
218+
const dispatcher = createDispatcher({ localConfig: null, handlers: [
219+
{ event: 'session-start', matcher: '*', handler: createHandler('cursor-user-rules', context('RULES')), yieldsToContextLimit: true },
220+
{ event: 'session-start', matcher: '*', handler: createHandler('team-env-hint', context('ENV')) },
221+
] });
222+
223+
const result = await dispatcher.dispatch('session-start', '*', {}, 'cursor');
224+
225+
expect(JSON.parse(result.output!).hookSpecificOutput.additionalContext).toBe('RULES\nENV');
226+
});
227+
188228
it('merges additionalContext from concurrent handlers', async () => {
189229
const first = createHandler('votes', JSON.stringify({
190230
hookSpecificOutput: { hookEventName: 'Stop', additionalContext: 'VOTES' },

‎src/cursor-user-rules.ts‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Cursor's user-scope rules outside the home directory (#1197).
3+
*
4+
* Cursor (IDE and CLI) has no direct read of `~/.cursor/rules`. It walks up
5+
* from the workspace root and loads `<ancestor>/.cursor/rules` at each step
6+
* (`LocalCursorRulesService.loadRulesFromDirAndAncestors`), so the file a
7+
* user-scope pull writes reaches only a workspace under the home directory.
8+
* Anywhere else, teamai's sessionStart hook adds the same rules and
9+
* instruction blocks; under the home directory it adds nothing, since the
10+
* walk already loads the files.
11+
*/
12+
import fs from 'node:fs';
13+
import path from 'node:path';
14+
import { loadLocalConfig, loadTeamConfig } from './config.js';
15+
import { CURSOR_HOOK_CONTEXT_LIMIT } from './hook-dispatch.js';
16+
import { instructionHookTextFor } from './instruction-targets.js';
17+
import { teamRulesContext } from './resources/rules.js';
18+
import { isAgentExcluded, resolveToolBaseDir, scopedToolPaths, type LocalConfig, type TeamaiConfig } from './types.js';
19+
import { log } from './utils/logger.js';
20+
21+
async function canonicalPath(p: string): Promise<string> {
22+
const resolved = await fs.promises.realpath(p).catch(() => path.resolve(p));
23+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
24+
}
25+
26+
/** Whether Cursor's walk up from `workspace` reaches `rulesDir`, directly or through a link on the way. */
27+
async function cursorWalkReaches(workspace: string, rulesDir: string): Promise<boolean> {
28+
const target = await canonicalPath(rulesDir);
29+
let dir = path.resolve(workspace);
30+
for (;;) {
31+
if (await canonicalPath(path.join(dir, '.cursor', 'rules')) === target) return true;
32+
const parent = path.dirname(dir);
33+
if (parent === dir) return false;
34+
dir = parent;
35+
}
36+
}
37+
38+
/** The user-scope rules directory a pull writes Cursor's rules to, or undefined when Cursor gets none. */
39+
export function cursorUserRulesDir(teamConfig: TeamaiConfig, userConfig: LocalConfig): string | undefined {
40+
const rules = scopedToolPaths(teamConfig, userConfig).cursor?.rules;
41+
return rules === undefined ? undefined : path.join(resolveToolBaseDir('cursor', userConfig), rules);
42+
}
43+
44+
/** The user-scope instruction blocks and team rules, as the hook adds them; null when there are none. */
45+
export async function cursorUserRulesText(teamConfig: TeamaiConfig, userConfig: LocalConfig): Promise<string | null> {
46+
const parts = [await instructionHookTextFor(teamConfig, userConfig, 'cursor'), await teamRulesContext(teamConfig, userConfig)]
47+
.filter((part): part is string => !!part);
48+
return parts.length === 0 ? null : parts.join('\n\n');
49+
}
50+
51+
/**
52+
* What the sessionStart hook adds for a Cursor session in `workspace`: the
53+
* user-scope rules when the walk does not reach their directory, else null.
54+
* `config` is the scope the hook resolved for the workspace; a project there
55+
* delivers its own rules into the workspace, so the user scope is loaded apart.
56+
*/
57+
export async function cursorUserRulesContext(workspace: string, config: LocalConfig): Promise<string | null> {
58+
const userConfig = config.scope === 'user' ? config : await loadLocalConfig();
59+
if (!userConfig || isAgentExcluded(userConfig, 'cursor')) return null;
60+
const teamConfig = await loadTeamConfig(userConfig.repo.localPath);
61+
if (!teamConfig) return null;
62+
const rulesDir = cursorUserRulesDir(teamConfig, userConfig);
63+
if (rulesDir === undefined || await cursorWalkReaches(workspace, rulesDir)) return null;
64+
const text = await cursorUserRulesText(teamConfig, userConfig);
65+
if (text !== null && text.length > CURSOR_HOOK_CONTEXT_LIMIT) {
66+
log.debug(`cursor-user-rules: ${text.length} characters, over Cursor's ${CURSOR_HOOK_CONTEXT_LIMIT}-character hook limit; not added`);
67+
return null;
68+
}
69+
return text;
70+
}

0 commit comments

Comments
 (0)