Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,9 @@ Code's settings file as `permissions.deny` entries (`Read(<pattern>)`).

Reasonix has no ignore file either, so its deny list goes into the `[permissions]` table of the shared `reasonix.toml` (project) / `~/.reasonix/config.toml` (global, via `--global`) as `Read(<pattern>)` entries — the same Claude-Code-style rule syntax the permissions feature writes there. `deny` is used rather than `[sandbox].forbid_read` because deny rules take glob specifiers and are documented as "a hard block in every mode", while `forbid_read` takes absolute paths with no documented glob support. The file is shared with the MCP and permissions features: only `Read(...)` deny entries are replaced, every other table and deny entry is preserved, and the file is never deleted. When the permissions feature also manages the `Read` category its explicit rules win, and the overwrite is warned about. As with the MCP and permissions features, the file is re-serialized on write, so hand-written comments, blank lines, and key ordering in `reasonix.toml` are not preserved.

Kiro reads `.kiroignore` in project scope and `~/.kiro/settings/kiroignore` in user scope. The `kiro`, `kiro-cli`, and `kiro-ide` targets therefore support `--global` for the deprecated ignore feature, as does `reasonix` (whose config file exists in both scopes); the remaining ignore targets are project-only.
Kiro reads `.kiroignore` in project scope and `~/.kiro/settings/kiroignore` in user scope. The `kiro`, `kiro-cli`, and `kiro-ide` targets therefore support `--global` for the deprecated ignore feature, as do `reasonix` and `zed` (whose config files exist in both scopes); the remaining ignore targets are project-only.

Zed has no ignore file: its deny list is the `private_files` array inside the shared settings file — `.zed/settings.json` in project scope and `~/.config/zed/settings.json` in global scope (`%APPDATA%\Zed\settings.json` on Windows). `private_files` is a worktree setting, and Zed layers default → user → project, so the key is honored in the user settings file too. The array is **owned wholesale by Rulesync**: it is replaced with the patterns from `.rulesync/.aiignore` on every generation, so a pattern deleted there is retracted from `settings.json` instead of surviving forever. When no patterns remain at all, the key is removed rather than written as `[]` — Zed ships a populated default `private_files` (`**/.env*`, `**/*.pem`, …) that any user or project value replaces wholesale, so an empty array would switch its secret redaction off. Every other key in the file — including the MCP `context_servers` and permissions `agent` blocks and unrelated editor settings — is preserved, and the file is never deleted.

Goose retired `.gooseignore` upstream ("removed some time ago in favour of other ignore things like gitignore etc" — [goose#10343](https://github.com/aaif-goose/goose/issues/10343)), so rulesync no longer generates it; the replacement guidance is `.gitignore` plus tool permissions. Stale `.gooseignore` files from earlier versions stay gitignored but are not cleaned up automatically.

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Warp | warp | ✅ | ✅ | ✅ 🌏 | ✅ 🌏 | | ✅ 🌏 | | 🌏 | |
| Replit | replit | ✅ | | | | | ✅ 🌏 | | | |
| Pi Coding Agent | pi | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | | |
| Zed | zed | ✅ 🌏 | | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | |
| Zed | zed | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | |

<!-- SUPPORTED_TOOLS_DOCS:END -->

Expand Down
4 changes: 3 additions & 1 deletion skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,9 @@ Code's settings file as `permissions.deny` entries (`Read(<pattern>)`).

Reasonix has no ignore file either, so its deny list goes into the `[permissions]` table of the shared `reasonix.toml` (project) / `~/.reasonix/config.toml` (global, via `--global`) as `Read(<pattern>)` entries — the same Claude-Code-style rule syntax the permissions feature writes there. `deny` is used rather than `[sandbox].forbid_read` because deny rules take glob specifiers and are documented as "a hard block in every mode", while `forbid_read` takes absolute paths with no documented glob support. The file is shared with the MCP and permissions features: only `Read(...)` deny entries are replaced, every other table and deny entry is preserved, and the file is never deleted. When the permissions feature also manages the `Read` category its explicit rules win, and the overwrite is warned about. As with the MCP and permissions features, the file is re-serialized on write, so hand-written comments, blank lines, and key ordering in `reasonix.toml` are not preserved.

Kiro reads `.kiroignore` in project scope and `~/.kiro/settings/kiroignore` in user scope. The `kiro`, `kiro-cli`, and `kiro-ide` targets therefore support `--global` for the deprecated ignore feature, as does `reasonix` (whose config file exists in both scopes); the remaining ignore targets are project-only.
Kiro reads `.kiroignore` in project scope and `~/.kiro/settings/kiroignore` in user scope. The `kiro`, `kiro-cli`, and `kiro-ide` targets therefore support `--global` for the deprecated ignore feature, as do `reasonix` and `zed` (whose config files exist in both scopes); the remaining ignore targets are project-only.

Zed has no ignore file: its deny list is the `private_files` array inside the shared settings file — `.zed/settings.json` in project scope and `~/.config/zed/settings.json` in global scope (`%APPDATA%\Zed\settings.json` on Windows). `private_files` is a worktree setting, and Zed layers default → user → project, so the key is honored in the user settings file too. The array is **owned wholesale by Rulesync**: it is replaced with the patterns from `.rulesync/.aiignore` on every generation, so a pattern deleted there is retracted from `settings.json` instead of surviving forever. When no patterns remain at all, the key is removed rather than written as `[]` — Zed ships a populated default `private_files` (`**/.env*`, `**/*.pem`, …) that any user or project value replaces wholesale, so an empty array would switch its secret redaction off. Every other key in the file — including the MCP `context_servers` and permissions `agent` blocks and unrelated editor settings — is preserved, and the file is never deleted.

Goose retired `.gooseignore` upstream ("removed some time ago in favour of other ignore things like gitignore etc" — [goose#10343](https://github.com/aaif-goose/goose/issues/10343)), so rulesync no longer generates it; the replacement guidance is `.gitignore` plus tool permissions. Stale `.gooseignore` files from earlier versions stay gitignored but are not cleaned up automatically.

Expand Down
2 changes: 1 addition & 1 deletion skills/rulesync/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Warp | warp | ✅ | ✅ | ✅ 🌏 | ✅ 🌏 | | ✅ 🌏 | | 🌏 | |
| Replit | replit | ✅ | | | | | ✅ 🌏 | | | |
| Pi Coding Agent | pi | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | | |
| Zed | zed | ✅ 🌏 | | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | |
| Zed | zed | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | | ✅ 🌏 | | ✅ 🌏 | |

<!-- SUPPORTED_TOOLS_DOCS:END -->

Expand Down
9 changes: 9 additions & 0 deletions src/constants/zed-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ export const ZED_GLOBAL_WIN32_DIR = join("AppData", "Roaming", "Zed");
export function getZedGlobalDir(): string {
return process.platform === "win32" ? ZED_GLOBAL_WIN32_DIR : ZED_GLOBAL_DIR;
}

/**
* The global config dir of the OTHER platform. `getZedGlobalDir()` resolves one
* spelling per platform, but the shared-write derivation (and the gateway
* ownership table it is checked against) must know both on every platform.
*/
export function getZedOtherPlatformGlobalDir(): string {
return process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR;
}
export const ZED_SETTINGS_FILE_NAME = "settings.json";
export const ZED_RULE_FILE_NAME = ".rules";
export const ZED_GLOBAL_RULE_FILE_NAME = "AGENTS.md";
Expand Down
25 changes: 25 additions & 0 deletions src/e2e/e2e-ignore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
REASONIX_PROJECT_PERMISSIONS_FILE_NAME,
} from "../constants/reasonix-paths.js";
import { RULESYNC_AIIGNORE_RELATIVE_FILE_PATH } from "../constants/rulesync-paths.js";
import { getZedGlobalDir, ZED_SETTINGS_FILE_NAME } from "../constants/zed-paths.js";
import { IgnoreProcessor } from "../features/ignore/ignore-processor.js";
import { fileExists, readFileContent, writeFileContent } from "../utils/file.js";
import {
Expand Down Expand Up @@ -297,6 +298,10 @@ describe("E2E: ignore (global mode)", () => {
target: "reasonix",
outputPath: join(REASONIX_GLOBAL_DIR, REASONIX_GLOBAL_PERMISSIONS_FILE_NAME),
},
{
target: "zed",
outputPath: join(getZedGlobalDir(), ZED_SETTINGS_FILE_NAME),
},
] as const;

it("global matrix must cover every native global ignore tool target", () => {
Expand Down Expand Up @@ -330,6 +335,26 @@ describe("E2E: ignore (global mode)", () => {
},
);

it("should import the Zed user-level private_files list", async () => {
const projectDir = getProjectDir();
const homeDir = getHomeDir();
await writeFileContent(
join(homeDir, getZedGlobalDir(), ZED_SETTINGS_FILE_NAME),
JSON.stringify({ theme: "One Dark", private_files: ["private/", "*.pem"] }, null, 2),
);

await runImport({
target: "zed",
features: "ignore",
global: true,
env: { HOME_DIR: homeDir },
});

const imported = await readFileContent(join(projectDir, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH));
expect(imported).toContain("private/");
expect(imported).toContain("*.pem");
});

it("should import the Kiro CLI user-level ignore file", async () => {
const projectDir = getProjectDir();
const homeDir = getHomeDir();
Expand Down
1 change: 1 addition & 0 deletions src/features/ignore/ignore-processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ describe("IgnoreProcessor", () => {
"kiro-cli",
"kiro-ide",
"reasonix",
"zed",
]);
});
});
Expand Down
8 changes: 7 additions & 1 deletion src/features/ignore/ignore-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ export const toolIgnoreFactories = new Map<IgnoreProcessorToolTarget, ToolIgnore
]);

const ignoreProcessorToolTargets: ToolTarget[] = [...toolIgnoreFactories.keys()];
const ignoreProcessorGlobalToolTargets: ToolTarget[] = ["kiro", "kiro-cli", "kiro-ide", "reasonix"];
const ignoreProcessorGlobalToolTargets: ToolTarget[] = [
"kiro",
"kiro-cli",
"kiro-ide",
"reasonix",
"zed",
];

type GetFactory = (target: IgnoreProcessorToolTarget) => ToolIgnoreFactory;

Expand Down
103 changes: 99 additions & 4 deletions src/features/ignore/zed-ignore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
RULESYNC_RELATIVE_DIR_PATH,
} from "../../constants/rulesync-paths.js";
import { getZedGlobalDir, getZedOtherPlatformGlobalDir } from "../../constants/zed-paths.js";
import { setupTestDirectory } from "../../test-utils/test-directories.js";
import { ensureDir, writeFileContent } from "../../utils/file.js";
import { RulesyncIgnore } from "./rulesync-ignore.js";
Expand Down Expand Up @@ -126,6 +127,30 @@ describe("ZedIgnore", () => {
relativeFilePath: "settings.json",
});
});

it("should return the platform-aware user config dir in global mode", () => {
const paths = ZedIgnore.getSettablePaths({ global: true });

expect(paths).toEqual({
relativeDirPath: getZedGlobalDir(),
relativeFilePath: "settings.json",
});
});
});

describe("getExtraSharedWritePaths", () => {
it("should declare no extra paths in project mode", () => {
expect(ZedIgnore.getExtraSharedWritePaths()).toEqual([]);
});

it("should declare the other platform's global settings file", () => {
expect(ZedIgnore.getExtraSharedWritePaths({ global: true })).toEqual([
{
relativeDirPath: getZedOtherPlatformGlobalDir(),
relativeFilePath: "settings.json",
},
]);
});
});

describe("isDeletable", () => {
Expand Down Expand Up @@ -231,7 +256,7 @@ describe("ZedIgnore", () => {
expect(jsonValue.private_files).toEqual(["*.log", ".env", "node_modules/**"]);
});

it("should preserve existing private_files while adding new ones", async () => {
it("should replace existing private_files so removed patterns are retracted", async () => {
const existingJsonContent = JSON.stringify(
{
private_files: ["existing.log", "*.log"],
Expand All @@ -256,7 +281,7 @@ describe("ZedIgnore", () => {
});

const jsonValue = JSON.parse(zedIgnore.getFileContent());
expect(jsonValue.private_files).toEqual(["*.log", "existing.log", "node_modules/**"]);
expect(jsonValue.private_files).toEqual(["*.log", "node_modules/**"]);
});

it("should handle patterns with comments and empty lines", async () => {
Expand Down Expand Up @@ -305,7 +330,7 @@ describe("ZedIgnore", () => {
const jsonValue = JSON.parse(zedIgnore.getFileContent());
expect(jsonValue.other).toBe("property");
expect(jsonValue.another).toBe("value");
expect(jsonValue.private_files).toEqual(["*.log", "secret.txt"]);
expect(jsonValue.private_files).toEqual(["*.log"]);
});

it("should remove duplicates and sort patterns", async () => {
Expand Down Expand Up @@ -333,7 +358,7 @@ describe("ZedIgnore", () => {
});

const jsonValue = JSON.parse(zedIgnore.getFileContent());
expect(jsonValue.private_files).toEqual(["*.log", "a.txt", "b.txt", "z.txt"]);
expect(jsonValue.private_files).toEqual(["*.log", "a.txt", "b.txt"]);
});

it("should handle Windows line endings", async () => {
Expand All @@ -353,6 +378,59 @@ describe("ZedIgnore", () => {
expect(jsonValue.private_files).toEqual(["*.log", ".env", "node_modules/**"]);
});

it("should write to the global settings file and preserve unrelated keys in global mode", async () => {
const globalDir = join(testDir, getZedGlobalDir());
await ensureDir(globalDir);
await writeFileContent(
join(globalDir, "settings.json"),
JSON.stringify({ theme: "One Dark", private_files: ["stale.txt"] }, null, 2),
);

const rulesyncIgnore = new RulesyncIgnore({
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
fileContent: "*.log\n.env",
});

const zedIgnore = await ZedIgnore.fromRulesyncIgnore({
outputRoot: testDir,
rulesyncIgnore,
global: true,
});

expect(zedIgnore.getRelativeDirPath()).toBe(getZedGlobalDir());
expect(zedIgnore.getRelativeFilePath()).toBe("settings.json");
const jsonValue = JSON.parse(zedIgnore.getFileContent());
expect(jsonValue.theme).toBe("One Dark");
expect(jsonValue.private_files).toEqual(["*.log", ".env"]);
});

it("should remove private_files entirely when no patterns remain", async () => {
const zedDir = join(testDir, ".zed");
await ensureDir(zedDir);
await writeFileContent(
join(zedDir, "settings.json"),
JSON.stringify({ theme: "One Dark", private_files: ["stale.txt"] }, null, 2),
);

const rulesyncIgnore = new RulesyncIgnore({
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
relativeFilePath: RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
fileContent: "# only a comment\n\n",
});

const zedIgnore = await ZedIgnore.fromRulesyncIgnore({
outputRoot: testDir,
rulesyncIgnore,
});

// An empty array would replace Zed's populated default `private_files`
// wholesale and switch its secret redaction off, so the key is dropped.
const jsonValue = JSON.parse(zedIgnore.getFileContent());
expect(jsonValue).not.toHaveProperty("private_files");
expect(jsonValue.theme).toBe("One Dark");
});

it("should create new JSON file when none exists", async () => {
const rulesyncIgnore = new RulesyncIgnore({
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
Expand Down Expand Up @@ -415,6 +493,23 @@ describe("ZedIgnore", () => {
expect(zedIgnore.getPatterns()).toEqual(["*.log", "node_modules/**"]);
});

it("should read the global settings file and import its patterns", async () => {
const globalDir = join(testDir, getZedGlobalDir());
await ensureDir(globalDir);
await writeFileContent(
join(globalDir, "settings.json"),
JSON.stringify({ theme: "One Dark", private_files: ["private/", "*.pem"] }, null, 2),
);

const zedIgnore = await ZedIgnore.fromFile({
outputRoot: testDir,
global: true,
});

expect(zedIgnore.getRelativeDirPath()).toBe(getZedGlobalDir());
expect(zedIgnore.toRulesyncIgnore().getFileContent()).toBe("private/\n*.pem");
});

it("should read file with validation enabled by default", async () => {
const jsonContent = JSON.stringify(
{
Expand Down
Loading
Loading