From 584169d49f23d3a1c3cf10480c5c6b837b402dd7 Mon Sep 17 00:00:00 2001
From: Jean-Baptiste THERY
Date: Wed, 28 Oct 2026 00:03:09 +0700
Subject: [PATCH 1/3] feat(hooks): install the index refresh into husky scripts
husky 9 points core.hooksPath at .husky/_, whose shims it regenerates on every install, and husky 8
at .husky itself. `rgr hooks install` refused both as a tracked hooks folder. It now recognizes
husky, writes the Ragmir block into the .husky/ scripts husky runs, reports manager=husky,
and asks to commit those scripts. Other tracked hooks folders still need --force.
---
docs/cli-reference.md | 6 ++-
packages/ragmir-core/src/cli.ts | 9 ++++-
packages/ragmir-core/src/git-hooks.test.ts | 43 ++++++++++++++++++++--
packages/ragmir-core/src/git-hooks.ts | 28 +++++++++++++-
4 files changed, 78 insertions(+), 8 deletions(-)
diff --git a/docs/cli-reference.md b/docs/cli-reference.md
index 1bf8aec..6c4da57 100644
--- a/docs/cli-reference.md
+++ b/docs/cli-reference.md
@@ -99,8 +99,10 @@ rgr hooks uninstall
Each hook gets one marked block that runs `node .ragmir/run.cjs ingest` in the background from the
knowledge base folder; existing hook content is kept, and a final `exit` stays last. Incremental
ingestion only re-embeds changed files. `hooks install` resolves `core.hooksPath` and linked
-worktrees, and refuses a hooks folder tracked in the working tree (for example `.husky/`) unless
-`--force` is given. Hooks need Git; on Windows they run in Git's bundled shell.
+worktrees. With husky, it edits the `.husky/` scripts that husky runs (`manager=husky`) rather
+than the `.husky/_` shims it regenerates; commit those scripts to share the refresh. Another hooks
+folder tracked in the working tree (for example `.githooks/`) needs `--force`. Hooks need Git; on
+Windows they run in Git's bundled shell.
## Safe upgrades
diff --git a/packages/ragmir-core/src/cli.ts b/packages/ragmir-core/src/cli.ts
index bb190e0..31e9c54 100644
--- a/packages/ragmir-core/src/cli.ts
+++ b/packages/ragmir-core/src/cli.ts
@@ -1268,7 +1268,10 @@ const hooksCommand = program
hooksCommand
.command("install")
.description("Add a background `rgr ingest` to the Git hooks of this repository.")
- .option("--force", "Also edit hooks in a directory tracked by the working tree, such as .husky.")
+ .option(
+ "--force",
+ "Also edit hooks in a directory tracked by the working tree, such as .githooks; husky is supported.",
+ )
.option("--json", "Print machine-readable JSON.")
.action(async (options: { force?: boolean; json?: boolean }, command: Command) => {
const report = await installGitHooks({
@@ -1698,7 +1701,11 @@ function printGitHooks(
)
}
console.log(`hooksDir=${report.hooksDir}`)
+ console.log(`manager=${report.manager}`)
console.log(`installed=${report.installed.join(",") || "none"}`)
+ if (action === "installed" && report.manager === "husky" && report.changed.length > 0) {
+ console.log("Commit the changed .husky scripts so the refresh follows the repository.")
+ }
if (report.missing.length > 0) {
console.log(`missing=${report.missing.join(",")}`)
}
diff --git a/packages/ragmir-core/src/git-hooks.test.ts b/packages/ragmir-core/src/git-hooks.test.ts
index ffc7a8e..b504c8f 100644
--- a/packages/ragmir-core/src/git-hooks.test.ts
+++ b/packages/ragmir-core/src/git-hooks.test.ts
@@ -1,6 +1,6 @@
import { execFileSync } from "node:child_process"
import { existsSync } from "node:fs"
-import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
+import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { afterEach, describe, expect, it } from "vitest"
@@ -62,12 +62,49 @@ describe("git hooks", () => {
it("refuses to edit a hooks directory tracked in the working tree unless forced", async () => {
const root = await gitRepository()
- execFileSync("git", ["config", "core.hooksPath", ".husky"], { cwd: root })
+ execFileSync("git", ["config", "core.hooksPath", ".githooks"], { cwd: root })
await expect(installGitHooks({ cwd: root })).rejects.toThrow(/inside the working tree/u)
const forced = await installGitHooks({ cwd: root, force: true })
- expect(forced.trackedHooksDir).toBe(true)
+ expect(forced).toMatchObject({ manager: "git", trackedHooksDir: true })
+ expect(existsSync(path.join(root, ".githooks", "post-merge"))).toBe(true)
+ })
+
+ it("adds the block to husky 9 scripts instead of the shims husky regenerates", async () => {
+ const root = await gitRepository()
+ const shims = path.join(root, ".husky", "_")
+ await mkdir(shims, { recursive: true })
+ await writeFile(path.join(shims, "h"), "#!/usr/bin/env sh\n", "utf8")
+ await writeFile(path.join(shims, ".gitignore"), "*", "utf8")
+ await writeFile(path.join(root, ".husky", "pre-commit"), "pnpm lint\n", "utf8")
+ execFileSync("git", ["config", "core.hooksPath", ".husky/_"], { cwd: root })
+
+ const installed = await installGitHooks({ cwd: root })
+ const postCommit = await readFile(path.join(root, ".husky", "post-commit"), "utf8")
+ const removed = await uninstallGitHooks({ cwd: root })
+
+ expect(installed).toMatchObject({
+ manager: "husky",
+ hooksDir: path.join(await realpath(root), ".husky"),
+ installed: ["post-checkout", "post-commit", "post-merge", "post-rewrite"],
+ })
+ expect(postCommit).toContain("node .ragmir/run.cjs ingest")
+ expect(existsSync(path.join(shims, "post-commit"))).toBe(false)
+ expect(await readFile(path.join(root, ".husky", "pre-commit"), "utf8")).toBe("pnpm lint\n")
+ expect(removed.changed).toHaveLength(4)
+ expect(existsSync(path.join(root, ".husky", "post-commit"))).toBe(false)
+ })
+
+ it("recognizes husky 8, whose hooks path is the scripts folder itself", async () => {
+ const root = await gitRepository()
+ await mkdir(path.join(root, ".husky", "_"), { recursive: true })
+ await writeFile(path.join(root, ".husky", "_", "husky.sh"), "#!/usr/bin/env sh\n", "utf8")
+ execFileSync("git", ["config", "core.hooksPath", ".husky"], { cwd: root })
+
+ const installed = await installGitHooks({ cwd: root })
+
+ expect(installed).toMatchObject({ manager: "husky", trackedHooksDir: true })
expect(existsSync(path.join(root, ".husky", "post-merge"))).toBe(true)
})
})
diff --git a/packages/ragmir-core/src/git-hooks.ts b/packages/ragmir-core/src/git-hooks.ts
index f846695..9248f6d 100644
--- a/packages/ragmir-core/src/git-hooks.ts
+++ b/packages/ragmir-core/src/git-hooks.ts
@@ -27,6 +27,8 @@ export interface GitHooksOptions {
export interface GitHooksReport {
hooksDir: string
workTree: string
+ /** `husky` when husky owns the hooks: Ragmir then edits its tracked `.husky/` scripts. */
+ manager: GitHooksManager
/** The hooks directory lives inside the working tree, so its files are usually committed. */
trackedHooksDir: boolean
installed: GitHookName[]
@@ -41,7 +43,7 @@ export interface GitHooksReport {
*/
export async function installGitHooks(options: GitHooksOptions = {}): Promise {
const location = await gitHooksLocation(options.cwd)
- if (location.trackedHooksDir && options.force !== true) {
+ if (location.trackedHooksDir && location.manager !== "husky" && options.force !== true) {
throw new Error(
`Git hooks resolve to ${location.hooksDir}, inside the working tree (for example a husky directory). Add the Ragmir block to that tool's hook files, or rerun with --force to edit them.`,
)
@@ -90,9 +92,12 @@ export async function gitHooksStatus(options: GitHooksOptions = {}): Promise
)
// Resolves core.hooksPath and linked worktrees the way Git itself does.
const hooksPath = (await git(projectRoot, ["rev-parse", "--git-path", "hooks"])).trim()
- const hooksDir = path.resolve(projectRoot, hooksPath)
+ const gitHooksDir = path.resolve(projectRoot, hooksPath)
+ const huskyDir = huskyScriptsDir(gitHooksDir)
+ const hooksDir = huskyDir ?? gitHooksDir
const gitDir = path.resolve(
projectRoot,
(await git(projectRoot, ["rev-parse", "--git-common-dir"])).trim(),
@@ -116,11 +123,27 @@ async function gitHooksLocation(cwd = process.cwd()): Promise
return {
hooksDir,
workTree,
+ manager: huskyDir === null ? "git" : "husky",
trackedHooksDir: insideWorkTree && !insideGitDir,
projectPath: canonicalSourcePath(path.relative(workTree, projectRoot)) || ".",
}
}
+/**
+ * The folder of husky's hook scripts when husky owns the Git hooks. husky 9 points core.hooksPath at
+ * `.husky/_`, which it regenerates on every install and whose shims run `.husky/`; husky 8
+ * points it at `.husky` itself. Hooks belong in the `.husky/` scripts in both cases.
+ */
+function huskyScriptsDir(gitHooksDir: string): string | null {
+ if (path.basename(gitHooksDir) === "_" && path.basename(path.dirname(gitHooksDir)) === ".husky") {
+ return existsSync(path.join(gitHooksDir, "h")) ? path.dirname(gitHooksDir) : null
+ }
+ if (path.basename(gitHooksDir) === ".husky") {
+ return existsSync(path.join(gitHooksDir, "_", "husky.sh")) ? gitHooksDir : null
+ }
+ return null
+}
+
function ragmirHookBlock(projectPath: string): string {
const runner = `${RAGMIR_DIR}/${RGR_RUNNER_FILENAME}`
return [
@@ -190,6 +213,7 @@ async function reportFor(
return {
hooksDir: location.hooksDir,
workTree: location.workTree,
+ manager: location.manager,
trackedHooksDir: location.trackedHooksDir,
installed,
missing,
From 548d943badd348c37c9a9929e9d8d0a5417bfb35 Mon Sep 17 00:00:00 2001
From: Jean-Baptiste THERY
Date: Wed, 28 Oct 2026 00:03:10 +0700
Subject: [PATCH 2/3] fix(mcp): name the resolved allowed roots when refusing a
workspace
The refusal printed the raw mcpAllowedRoots values, so an umbrella base configured with "." told
agents it served "workspaces under .". It now prints the resolved folders the check uses.
---
packages/ragmir-core/src/mcp-scope.ts | 5 ++++-
packages/ragmir-core/src/mcp.test.ts | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/packages/ragmir-core/src/mcp-scope.ts b/packages/ragmir-core/src/mcp-scope.ts
index 3ec1502..414b28b 100644
--- a/packages/ragmir-core/src/mcp-scope.ts
+++ b/packages/ragmir-core/src/mcp-scope.ts
@@ -122,7 +122,10 @@ export function workspaceScopeRefusal(
name: string,
config: Config,
): string {
- return `This Ragmir server serves the "${name}" knowledge base only to workspaces under ${config.mcpAllowedRoots.join(", ")} (mcpAllowedRoots). Your workspace is ${verdict.workspaceRoots.join(", ")}. Register a project-scoped Ragmir server for this workspace instead.`
+ const allowedRoots = config.mcpAllowedRoots.map((root) =>
+ resolveAllowedRoot(config.projectRoot, root),
+ )
+ return `This Ragmir server serves the "${name}" knowledge base only to workspaces under ${allowedRoots.join(", ")} (mcpAllowedRoots). Your workspace is ${verdict.workspaceRoots.join(", ")}. Register a project-scoped Ragmir server for this workspace instead.`
}
function resolveAllowedRoot(projectRoot: string, value: string): string {
diff --git a/packages/ragmir-core/src/mcp.test.ts b/packages/ragmir-core/src/mcp.test.ts
index d5a6fbc..2f2386a 100644
--- a/packages/ragmir-core/src/mcp.test.ts
+++ b/packages/ragmir-core/src/mcp.test.ts
@@ -608,7 +608,7 @@ describe("MCP workspace scope and session", () => {
})
expect(refused.isError).toBe(true)
- expect(textContent(refused)).toContain("only to workspaces under .")
+ expect(textContent(refused)).toContain(`only to workspaces under ${root} (mcpAllowedRoots)`)
expect(allowed.isError).not.toBe(true)
expect(allowed._meta?.["ragmir/workspace"]).toMatchObject({ relation: "related" })
})
From 10806e8aa5a1c72742db972edfbb97fb53dfb51e Mon Sep 17 00:00:00 2001
From: Jean-Baptiste THERY
Date: Wed, 28 Oct 2026 00:03:10 +0700
Subject: [PATCH 3/3] docs: add community, early quick start, and social badges
to both READMEs
Compared with eight successful open-source READMEs (Vite, Zod, tRPC, Biome, Orama, Context7, mem0,
Chroma), every convention most of them share is now present: the GitHub README shows a three-line
quick start in its first lines and a community section, and both READMEs carry downloads and stars
badges; the npm README gains the centered logo header.
---
README.md | 16 +++++++++++++++
packages/ragmir-core/README.md | 36 +++++++++++++++++++++++-----------
2 files changed, 41 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index a681450..bc8a456 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,8 @@
+
+
@@ -31,6 +33,12 @@ Code, Codex, or your own application queries it through MCP, the `rgr` CLI, or a
and every passage comes back with a citation the agent can open and check. Your agent plans,
reasons, and writes with the model you choose; Ragmir finds the evidence.
+```bash
+npm install -D @jcode.labs/ragmir
+npx rgr setup --agents claude,codex # index the project, connect your agents
+npx rgr search "how are permissions checked?" --compact
+```
+
| Without Ragmir | With Ragmir |
| --- | --- |
| Code search misses the specs, spreadsheets, and scanned PDFs around the code. | One local index for code and documents, with optional local OCR. |
@@ -282,6 +290,14 @@ then run `rgr ingest`. Monorepos can keep separate bases, selected with `rgr bas
- [Troubleshooting](./docs/troubleshooting.md)
- [Synthetic examples](./packages/ragmir-core/examples/sovereign-rag-demo/README.md)
+## Community
+
+Ask questions, propose ideas, and report bugs in
+[GitHub Issues](https://github.com/jcode-works/jcode-ragmir/issues); report vulnerabilities
+privately through [SECURITY](./SECURITY.md). Follow releases on
+[GitHub](https://github.com/jcode-works/jcode-ragmir/releases), and if Ragmir saves your agents
+time, [star the repository](https://github.com/jcode-works/jcode-ragmir) so other teams find it.
+
## Contributing
Issues and pull requests are welcome; read [CONTRIBUTING](./CONTRIBUTING.md) and the
diff --git a/packages/ragmir-core/README.md b/packages/ragmir-core/README.md
index 33f700d..cf2a1c7 100644
--- a/packages/ragmir-core/README.md
+++ b/packages/ragmir-core/README.md
@@ -1,11 +1,23 @@
-# @jcode.labs/ragmir
-
-[](https://www.npmjs.com/package/@jcode.labs/ragmir)
-[](https://github.com/jcode-works/jcode-ragmir/actions/workflows/ci.yml)
-[](https://github.com/jcode-works/jcode-ragmir/blob/main/LICENSE)
-[](https://nodejs.org)
-
-**Cited project context for coding agents. Local, fast, and offline by default.**
+
+
+
+
+
+
+@jcode.labs/ragmir
+
+
+ Cited project context for coding agents. Local, fast, and offline by default.
+
+
+
+
+
+
+
+
+
+
Ragmir indexes the code, specifications, and office documents you choose, then returns precise
passages with checkable citations through a TypeScript library, the `rgr` CLI, or a local MCP
@@ -143,9 +155,11 @@ Read the [full documentation](https://github.com/jcode-works/jcode-ragmir#readme
[CLI reference](https://github.com/jcode-works/jcode-ragmir/blob/main/docs/cli-reference.md),
[migration guide](https://github.com/jcode-works/jcode-ragmir/blob/main/docs/migration.md), and
[troubleshooting guide](https://github.com/jcode-works/jcode-ragmir/blob/main/docs/troubleshooting.md).
-Issues and contributions are welcome through
-[GitHub](https://github.com/jcode-works/jcode-ragmir) and
-[CONTRIBUTING](https://github.com/jcode-works/jcode-ragmir/blob/main/CONTRIBUTING.md).
+Ask questions and report bugs in
+[GitHub Issues](https://github.com/jcode-works/jcode-ragmir/issues); contributions are welcome
+through [CONTRIBUTING](https://github.com/jcode-works/jcode-ragmir/blob/main/CONTRIBUTING.md). If
+Ragmir saves your agents time, [star the repository](https://github.com/jcode-works/jcode-ragmir)
+so other teams find it.
Ragmir is open source under [AGPL-3.0-only](https://github.com/jcode-works/jcode-ragmir/blob/main/LICENSE).
JCode Works also offers a [commercial license](https://github.com/jcode-works/jcode-ragmir/blob/main/COMMERCIAL-LICENSE.md).