What's Changed
New Features
- A root
languagekey steers the language the AI answers in.rulesync.jsonc(andrulesync.local.jsonc, which overrides it per developer) acceptslanguagewith one ofen,ja,zh-CN,zh-TW,ko,fr,de,es,pt-BR,ru. Claude Code has a native setting, so forclaudecode/claudecode-legacytherulesfeature writes"language": "japanese"(and so on) into.claude/settings.local.json(project scope) or~/.claude/settings.json(global scope) and touches nothing else in the file, whileCLAUDE.mdis left as it is. Every other target gets the instruction simulated as one block appended after a thematic break to the generated root rule file only — nested rules never carry it.rulesync importrecognizes that block for every supported language and strips it (with a warning, since thelanguagekey is what keeps the instruction), so an import-then-generate loop yields exactly one block, andrulesync convertre-adds it to the destination's root file. When the key is omitted Rulesync says nothing about language, soenis an explicit instruction rather than the default, and an unset key never creates or modifies the Claude Code settings file (#2889, closes #2863). agentsmd.subprojectPathcan be derived from a rule'sglobs. A newrulesync.jsoncoptionderiveSubprojectPathFromGlobs(defaultfalse, also honored fromrulesync.local.jsonc) places each non-root rule as a nestedAGENTS.mdunder the directory its globs already point at (["packages/api/**/*"]becomespackages/api) for the targets that nest, so the directory no longer has to be repeated by hand. A single rule opts in withagentsmd: { subprojectPath: "auto" }regardless of the option, or opts out withagentsmd: { subprojectPath: "" }when the option is on; an explicit directory always wins androot: truerules never nest. Only an explicit"auto"that cannot be honored (no shared wildcard-free prefix, disagreeing globs, negation,.., an absolute path, or a root rule) warns, once per rule; config-driven derivation falls back silently so ordinary activation globs such as["src/**/*.ts", "test/**/*.ts"]stay quiet. The MCPruleoperations return the authored frontmatter, so a get, edit and put round trip keeps"auto"in the file (#2884, closes #2864).- Generated Claude Code settings files carry
$schema. Every generated.claude/settings.jsonand.claude/settings.local.json, in project and global scope, now starts with"$schema": "https://json.schemastore.org/claude-code-settings.json"whichever feature (permissions, ignore, or hooks) writes the file, so editors validate and autocomplete it. The key is only ensured, never forced: a$schemathe file already states is left untouched, whatever its value, and non-Claude shared files are unaffected (#2883, closes #2862). - Skill frontmatter accepts root-level
license,compatibilityandmetadata. In.rulesync/skills/<name>/SKILL.mdthese three fields can now be written once at the root instead of repeated per tool section. A root value applies to every target that models the field, and a tool section that sets the same key overrides it outright for that target only, the same precedence rule asdisable-model-invocation/user-invocable. The Agent Skills writers (agentsskills, hermesagent, agentsmd) apply their usual normalization to root values (acompatibilityobject becomes a string,metadatavalues are stringified), Copilot and Copilot CLI take onlylicense, Cursor takes onlymetadata, and Kilo now acceptscompatibilityas a string or an object like every other Agent Skills target (#2882, closes #2874). - A shared JSONC file that falls back to a whole-document rewrite is reported. v16.20.0 made
generateedit.vscode/settings.json,.amp/settings.json,opencode.json(c),kilo.json(c)and their kin in place, silently rewriting the whole document only when there is nothing to edit against. That rewrite now emits one warning per file per run naming the file and the reason: the file could not be parsed, its root is not an object, a key is stated twice, a prototype-pollution key is present, the write or edit budget was exceeded, or the editor threw. An empty file, the in-place edit path and the non-JSONC formats stay silent, so a comment that disappears is no longer a mystery (#2895, closes #2858).
Bug Fixes
- The skill picker counts East Asian Ambiguous characters as two columns. The width measurement behind the
fetchskill picker treated the UAX #11 Ambiguous class (box drawing, Greek, Cyrillic, accented Latin-1, the em dash, the ellipsis) at one column, which is what a Latin terminal draws, while a terminal set to draw the class wide, common in CJK locales, draws it at two. A skill name padded with box-drawing characters therefore passed the hidden-character check, fit the label budget, and was hard-wrapped by the terminal into a forged row with no pointer and no checkbox. The class is now counted at two columns, so such names are shortened before they can wrap; the cost is that names carrying Greek, Cyrillic or accented letters are cut a little earlier on Latin terminals (#2890, closes #2845). - deepagents no longer creates an empty global
config.toml. When no permission rule mapped to the deepagents allowlist, a fresh~/.deepagents/config.tomlholding a lone newline was created on a machine that had none. The file is deepagents' own: Rulesync merges into it when it exists and now has no reason to bring it into existence to hold nothing. An existing file is still rewritten as before, so no user content is dropped (#2891, closes #2859). fetchdrops skill names that Windows folds or aliases. A skill name with a trailing dot or space (deploy.,deploy) is resolved by Win32 to the plain name, and aNAME~<digits>name (DEPLOY~1) is an 8.3 short-name alias that opens whatever long name it stands for, so under the default--conflict overwritea remotedeploy.could replace the files of a localdeployskill the repository never published. Such names are now left out of the interactive picker, rejected by--skillsas unknown, never written, and reported through a warning that quotes each name as it is so a trailing space shows. Names such asdata~2parserandv1.0are still fetched (#2888, closes #2848).- OpenCode
skills.pathsentries are judged by where they resolve.importfiltered theskills.pathsentries ofopencode.json(c)only by their spelling, so a relative entry naming a symbolic link that points outside the config directory passed the check and itsSKILL.mdfiles were read. Each entry is now also resolved and dropped when it escapes the config directory, the same rule the nested Claude Code skills roots already follow; a link that stays inside is kept, and a non-existent entry is still accepted as before (#2887, closes #2850). - Nested
.claude/skillsroots recognize a symlinked project root. The nested-root scan for Claude Code skills is now seeded with both the literal.claude/skillsand where it resolves to, so a nested root that reaches the project's own symlinked skills directory is recognized as the tool's own root rather than read a second time. A root reached through a link is deliberately still recorded under the spelling the scan reported, since that is the path that is read and that the skill'spathsare scoped to; this behavior is now pinned and documented, alongside two clarity cleanups in the path helpers (#2885, closes #2852). - The skill picker's label budget honors
CLI_WIDTH. The inquirer renderer measures its width throughcli-width, which skips a zero-column TTY in favor of theCLI_WIDTHenvironment variable before falling back to 80, while the label budget read the terminal width directly. A TTY reporting zero columns under aCLI_WIDTHnarrower than 77 therefore had its labels cut to 72 while the rows were broken atCLI_WIDTH, letting a label wrap onto the second line the cap exists to prevent. The budget now parsesCLI_WIDTHthe way the renderer does when the terminal reports no width (#2886, closes #2847). - Executable bits on skill scripts survive
generateandfetch. A skill's supporting files were copied byte for byte but written as plain buffers, so a script that was executable in.rulesync/skills/<name>/scripts/lost its executable bit in every generated skill directory;fetchhad the same gap because the GitHub contents API carries no file mode.generatenow records and applies the mode of any executable supporting file, and restores a stripped bit even when the directory is otherwise unchanged.fetchreads the repository tree once and marks every path git records as100755executable; if the tree is truncated or unreadable the files are still fetched and a warning says the bit was not applied, so the loss is never silent. Nothing is applied in dry-run mode, and both paths are no-ops on Windows (#2880, closes #2866). generate --deleteprunes stale files inside generated skill directories. The orphan sweep removed a whole skill directory that no longer had a source and the flat files of flattening tools, but never looked inside a directory whose skill still existed. Deletingreferences/old.mdfrom.rulesync/skills/<name>/while keeping the skill therefore left the generated copy in place forever, andgenerate --checkreported the tree as up to date. A third sweep now removes files that no source produced from every directory Rulesync owns outright and generated in this run, and--checkreports them and exits1. Hidden entries such as.gitkeepand symbolic links are left alone because Rulesync never writes them, generated names are matched case-insensitively so a case-only rename does not delete the file it just wrote, and the sweep stands down when the source read was incomplete (#2876, closes #2867, closes #2739).- Cursor import no longer invents globs on Always Apply rules.
rulesync import --targets cursorrewrote an Always Apply.mdcrule with an empty or missingglobsintoglobs: ["**/*"]on both the canonicalglobsandcursor.globs, andgeneratewrote that glob back out next toalwaysApply: true, a pairing Cursor's documentation says is ignored and that some versions read as an Agent Requested rule. The canonicalglobsstill becomes["**/*"]so tools with no Always Apply equivalent keep applying the rule everywhere, butcursor.globsnow mirrors the source file and becomes an explicit[]when it carried no patterns, so the regenerated.mdcgoes back out without aglobsline. After upgrading, regenerate (or drop a stalecursor.globs) to clear rules imported earlier (#2875, closes #2868). - Amp settings are sanitized before both the permissions and MCP adapters read them.
.amp/settings.jsonis now parsed through the strict, sanitizing JSONC reader that rebuilds every object from its own entries, so__proto__,constructorandprototypekeys are removed recursively before the document is inspected or re-serialized. The permissions adapter got this first; the MCP adapter still held a near-identical copy of the old parser, through which a root__proto__could inject anamp.mcpServersentry that appears nowhere in the file and have it imported into generated output. Both adapters now share one parser, comments and trailing commas are still preserved, and the same malformed syntax is still rejected with the Amp-specific error context (#2860, closes #2855; #2873, closes #2869). - Every scan runs from a literal root, so a project path with glob metacharacters is read as a directory. The orphan sweeps and the input scans embedded the project root into their glob patterns, so a directory named
project(glob),project{a,b}orproject*xwas read as a pattern: files were skipped, or picked up from a sibling directory. The input side was the dangerous one: withproj(x)/.rulesync/rules/a.mdpresent the rule scan returned nothing, which read as "every rule was deleted", sogenerate --deleteremoved generated files it could no longer regenerate and still reported success. The subagents, commands and checks sweeps were converted first, and the rules, commands, skills and nested.claude/skillsscans now follow, each passing the root as a literal working directory with a root-relative pattern so a tool cannot reintroduce the bug by spelling the root into its own pattern (#2811; #2872, closes #2870).
Other Changes
- Every log line that quotes a remote-derived name now goes through one
quoteForLoghelper that strips control characters before JSON-quoting, replacing 45 hand-spelled copies; no log wording changes (#2893, closes #2842). - A
goal-issues-and-releaseproject skill was added under.rulesync/skills/for this repository's own unattended maintenance runs; it is not a distributed skill (#2877). - An official
rulesync-mcpskill was added and then reverted within this release, so nothing distributed changes (#2871, #2879). - Dependencies were bumped as a group (107 updates including
@inquirer/checkbox,fastmcp,js-yaml,@anthropic-ai/claude-agent-sdk,cspell,oxfmt,oxlint,secretlint), along with two GitHub Actions (#2809, #2808). - The Homebrew formula was updated to v16.20.0 (#2865).