feat(server): read file + list directory endpoints for external projects#120
Merged
Conversation
Add two read-only, external-project-only capabilities so agents (Claude
Code, and especially Cowork which has no local filesystem) can inspect the
actual contents and file tree of workspace / external repos the harness
cannot see locally:
- POST /api/v1/projects/{path}/file — read a file whole or by line range
- POST /api/v1/projects/{path}/tree — list a directory (one level, ls-like)
Both read from the server-side git worktree (the source of truth; the index
stores only chunks). Gated to EXTERNAL (GitHub-backed) projects via the
existing access.IsProjectExternal helper — local projects return 409 with a
hint to use native filesystem tools. Access is GroupRead (requireProjectAccess).
Safety + concurrency:
- safeJoin rejects path traversal (.., absolute, symlink escape)
- new internal/repolocks package: per-repo RWMutex shared between the file
read handlers (read-lock) and the clone worker's worktree rewrite
(write-lock around CloneOrFetch), so a read never observes a mid-reset tree
- size/line/entry caps with truncated flags
Surfaced across all interfaces:
- CLI: cix file <f> [--lines N:M], cix tree [dir]
- MCP: cix_file, cix_tree (external-only, described)
- skills: file/dir section in the three cix SKILL.md + cix-workspace notes,
new /cix:file and /cix:tree plugin commands
Tests: internal/httpapi/file_test.go covers gating (local 409, uncloned 409,
unauthorized 404, group-shared 200), path traversal, line ranges, tree
listing, and a required concurrency test (5 readers vs a non-atomic writer,
green under -race).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ud past byte cap Addresses review findings on the file/tree endpoints: - safeJoin now rejects the repo-root .git segment, so git metadata is unreadable via /file too (it was already hidden from /tree). First-segment check targets exactly the clone's own .git. - CLI and MCP renderers decide "no lines" on end_line < start_line (the server's authoritative signal) instead of Content == "", which wrongly swallowed a legitimate single-blank-line file. - ReadProjectFile returns 400 with an actionable detail when a line range begins past the 2 MiB read cap, instead of a confusing empty 200. Tests: .git paths added to the traversal test; new TestReadProjectFile_RangePastByteCap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, honest total_lines - file.go: take the per-repo read-lock BEFORE path validation so a concurrent clone can't swap in an escaping symlink between check and open (TOCTOU) - file.go: reject inverted line ranges (end<start) with 422 instead of silently clamping — enforced server-side for MCP/raw callers, not just the CLI - file.go: /tree now 400s on a malformed body instead of silently listing root - file.go: total_lines counts the whole file on disk (streams past the 2 MiB content cap) instead of just the buffered slice - repojobs: run the indexer's worktree walk under the per-repo read-lock so a concurrent clone reset can't tear the index across two revisions - repolocks: add panic-safe WithWrite/WithRead helpers (defer-unlock); the clone worker used a plain unlock that leaked the lock forever on a recovered panic - tests: symlink-swap TOCTOU guard, /tree access-gating, byte-cap total_lines, inverted-range 422, malformed-body 400, repolocks panic + mutual-exclusion, handleIndex-takes-read-lock, and a FileContent/DirectoryListing wire golden Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mode truncation warning - cmd/file.go: emit the truncation warning to stderr in --raw mode too, so `cix file … --raw > copy` no longer silently drops capped content - client/file_test: parse the server's FileContent/DirectoryListing wire shape (twin golden of the server-side contract test), assert 422/409/404/400 surface as errors, and that an unset range omits start/end from the request body - cmd/mcp_tools_test: formatFileContent/formatTree rendering, incl. the empty-range (end<start) and truncated signals the MCP tools rely on - cmd/file_test: raw-mode truncation-warning coverage (stdout vs stderr) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dvcdsys
added a commit
that referenced
this pull request
Jul 2, 2026
…ync in CI #120 added the `cix tree`/`cix file` guidance block to the plugin copy of the cix-workspace skill (and to skills/cix/SKILL.md and both cix-cowork skills) but missed the canonical skills/cix-workspace/SKILL.md — so sync-skills.sh --check drifted, and a plain `sync-skills.sh` run would have overwritten the plugin copy and deleted #120's addition. - Forward-port the block into canonical skills/cix-workspace/SKILL.md (plugin was ahead; copy plugin → canonical to restore byte-identity). - Align the investigator agent `description` quoting (canonical → quoted, matching the plugin bundle). - Wire sync-skills.sh --check into ci-plugin.yml and broaden its path triggers to plugins/** and skills/** so a canonical-only edit runs the drift check — previously only plugins/cix/** triggered it, the exact gap that let #120 drift through unnoticed. - Validate the cix-cowork plugin manifest in the jq step too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dvcdsys
added a commit
that referenced
this pull request
Jul 2, 2026
… deps) (#107) * ci(dependabot): target the develop branch for version updates Our flow is feature -> develop (dev/manual tests) -> main (release), but Dependabot defaulted to the repo default branch (main), so weekly version bumps landed straight on main, bypassing develop. Set target-branch: "develop" on all five update blocks (gomod x2, npm, docker, github-actions) so scheduled version PRs go to develop. Note: Dependabot reads this config from the default branch (main), so the change only takes effect once it reaches main. Security (alert-triggered) updates still target the default branch by platform design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(deps): bump the go group in /server with 6 updates (#102) * docs: add reindex-after-upgrade disclaimer to README * chore(deps): bump the go group in /server with 6 updates Bumps the go group in /server with 6 updates: | Package | From | To | | --- | --- | --- | | [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) | `0.135.0` | `0.140.0` | | [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.4` | `5.3.0` | | [github.com/oapi-codegen/runtime](https://github.com/oapi-codegen/runtime) | `1.4.0` | `1.4.2` | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.52.0` | `0.53.0` | | [golang.org/x/sync](https://github.com/golang/sync) | `0.20.0` | `0.21.0` | | [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) | `1.34.1` | `1.53.0` | Updates `github.com/getkin/kin-openapi` from 0.135.0 to 0.140.0 - [Release notes](https://github.com/getkin/kin-openapi/releases) - [Commits](getkin/kin-openapi@v0.135.0...v0.140.0) Updates `github.com/go-chi/chi/v5` from 5.2.4 to 5.3.0 - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](go-chi/chi@v5.2.4...v5.3.0) Updates `github.com/oapi-codegen/runtime` from 1.4.0 to 1.4.2 - [Release notes](https://github.com/oapi-codegen/runtime/releases) - [Commits](oapi-codegen/runtime@v1.4.0...v1.4.2) Updates `golang.org/x/crypto` from 0.52.0 to 0.53.0 - [Commits](golang/crypto@v0.52.0...v0.53.0) Updates `golang.org/x/sync` from 0.20.0 to 0.21.0 - [Commits](golang/sync@v0.20.0...v0.21.0) Updates `modernc.org/sqlite` from 1.34.1 to 1.53.0 - [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md) - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.34.1...v1.53.0) --- updated-dependencies: - dependency-name: github.com/getkin/kin-openapi dependency-version: 0.140.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go - dependency-name: github.com/go-chi/chi/v5 dependency-version: 5.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go - dependency-name: github.com/oapi-codegen/runtime dependency-version: 1.4.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go - dependency-name: golang.org/x/crypto dependency-version: 0.53.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go - dependency-name: golang.org/x/sync dependency-version: 0.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go - dependency-name: modernc.org/sqlite dependency-version: 1.53.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github.com/spf13/cobra from 1.8.0 to 1.10.2 in /cli in the go group (#97) * docs: add reindex-after-upgrade disclaimer to README * chore(deps): bump github.com/spf13/cobra in /cli in the go group Bumps the go group in /cli with 1 update: [github.com/spf13/cobra](https://github.com/spf13/cobra). Updates `github.com/spf13/cobra` from 1.8.0 to 1.10.2 - [Release notes](https://github.com/spf13/cobra/releases) - [Commits](spf13/cobra@v1.8.0...v1.10.2) --- updated-dependencies: - dependency-name: github.com/spf13/cobra dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps-dev): bump @babel/core from 7.29.0 to 7.29.7 in /server/dashboard (#103) * docs: add reindex-after-upgrade disclaimer to README * chore(deps-dev): bump @babel/core in /server/dashboard Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.29.0 to 7.29.7. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.29.7/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/core" dependency-version: 7.29.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump alpine from 3.20 to 3.24 in /server (#95) * docs: add reindex-after-upgrade disclaimer to README * chore(deps): bump alpine from 3.20 to 3.24 in /server Bumps alpine from 3.20 to 3.24. --- updated-dependencies: - dependency-name: alpine dependency-version: '3.24' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump actions/setup-go from 5 to 6 (#100) * docs: add reindex-after-upgrade disclaimer to README * chore(deps): bump actions/setup-go from 5 to 6 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](actions/setup-go@v5...v6) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump softprops/action-gh-release from 2 to 3 (#98) * docs: add reindex-after-upgrade disclaimer to README * chore(deps): bump softprops/action-gh-release from 2 to 3 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](softprops/action-gh-release@v2...v3) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump docker/login-action from 3 to 4 (#93) * docs: add reindex-after-upgrade disclaimer to README * chore(deps): bump docker/login-action from 3 to 4 Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](docker/login-action@v3...v4) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(ci): add Docker Hub tag retention + auto-prune (#105) The Docker Hub repo had grown past 150 GB — Docker Hub never prunes tags, so every release (~1 GB CUDA) plus scout/test/dev scan tags accumulated forever (53 tags, ~44 of them stale). Add server/scripts/dockerhub-prune.sh: a deny-by-default retention policy that keeps the floating tags (latest, cu128, develop-cu128) + the newest KEEP_RELEASES (default 3) semver versions, and deletes everything else (scout-*, *-cu130/-cu126, go-*, *-dev*, ci-test-*, unprefixed 0.x, old releases). DRY_RUN=true by default; portable across GNU + BSD (macOS) sort. Enforce it automatically: - release-server.yml: prune job after each release push (keep newest 3). - cleanup-dockerhub.yml: weekly sweep + manual dispatch (dry-run default). Both reuse the existing DOCKER_USERNAME / DOCKER_PASSWORD secrets. Deleted release tags remain rebuildable via the release workflow_dispatch path. Also fix portainer-stack-cuda.yml: it still referenced the obsolete `go-cu128` floating tag (renamed to `cu128` long ago) — a future redeploy from that file would have failed to pull once go-cu128 is pruned. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * chore(deps): bump actions/upload-artifact from 4 to 7 (#94) * docs: add reindex-after-upgrade disclaimer to README * chore(deps): bump actions/upload-artifact from 4 to 7 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](actions/upload-artifact@v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump actions/download-artifact from 4 to 8 (#99) * docs: add reindex-after-upgrade disclaimer to README * chore(deps): bump actions/download-artifact from 4 to 8 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](actions/download-artifact@v4...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump golang from 1.25-alpine to 1.26-alpine in /server (#101) * docs: add reindex-after-upgrade disclaimer to README * chore(deps): bump golang from 1.25-alpine to 1.26-alpine in /server Bumps golang from 1.25-alpine to 1.26-alpine. --- updated-dependencies: - dependency-name: golang dependency-version: 1.26-alpine dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(cowork): cix in Claude Cowork via MCP connector + skills plugin (#106) * feat(cowork): cix in Claude Cowork via MCP connector + skills plugin Claude Cowork doesn't load Claude Code plugins/hooks/slash-commands and doesn't shell out to the cix CLI, so cix was unreachable there — exactly where it's most useful (large cross-repo research). Expose it over MCP. Two-step, server-centric, multi-server: - `cix mcp`: stdio MCP server subcommand (cli/cmd/mcp.go, mcp_tools.go) as a thin front-end over the existing internal/client HTTP client. 11 tools — list servers/projects/workspaces, cross-project workspace_search, and per-project search/definitions/references/symbols/files/summary. No cwd inference and no "current project"; project (host_path) and workspace are explicit, server is an optional per-call selector resolved against a cached serverRegistry. - `cix mcp connect claude`: one-command installer (cli/cmd/mcp_connect.go) that registers this binary in claude_desktop_config.json via the official mcpServers mechanism — absolute path, preserves other servers, writes .bak, idempotent, --print dry-run, disconnect to undo, no secrets. - cli/mcpb/: optional .mcpb desktop-extension bundle (make mcpb) for redistributing the connector to machines without cix. - plugins/cix-cowork/: skills-only marketplace plugin (2nd entry) — the cix and cix-workspace skills adapted from the CLI to the cix_* MCP tools (workspace uses sequential cix_search drill-down, no sub-agent; trust-rules + troubleshooting in references/). Install after connecting the server. - doc/COWORK_MCP.md, marketplace 2nd entry, go.mod (MCP SDK), Makefile mcpb. - fix(plugin): quote the cix-workspace-investigator description so strict `claude plugin validate` passes (unquoted "Do not use for:" colon). The Claude Code `cix` plugin is unchanged (CLI + hooks); this is its Cowork counterpart over the same cix server. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: slim README to a landing page; add credits, Voyage, team-deploy guide Cut README from ~790 to ~320 lines by de-duplicating repeated notes (bootstrap admin, reindex-after-upgrade, drift, GPU) and relocating reference material into focused docs. No content lost — moved, not deleted. New docs: - doc/CLI_REFERENCE.md — full CLI surface + per-project config - doc/DASHBOARD.md — pages, auth, authorization model, drift - doc/TROUBLESHOOTING.md — common issues + search-quality tuning - doc/TEAM_DEPLOYMENT.md — self-hosting cix for a team (DevOps guide) README also gains an Embedding providers section (local / Voyage AI / OpenAI-compatible — Voyage was already a first-class provider but nearly invisible) and an Acknowledgements section crediting upstreams. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(cowork): MCP install/uninstall seam, drop .mcpb, fix min_score & partial-status Revise the Cowork MCP integration and fix three review findings. Connector UX & packaging: - Rename `cix mcp connect|disconnect claude` → `install|uninstall claude-desktop` behind a host-registry seam (mcpHosts/lookupHost), so adding hosts (e.g. Codex/TOML) is a table entry, not a new command. - Drop the `.mcpb` desktop-extension path (build.sh/manifest.json, Makefile target) — MCP connector + marketplace skills only. - Rewrite doc/COWORK_MCP.md and plugin docs to match. Review fixes (MCP must mirror the CLI — it is just another CLI surface): - cix_search: apply the documented default min_score 0.4 when omitted instead of silently sending nothing (server default 0.2). Single source of truth via searchDefaultMinScore, shared by the `cix search` flag and the MCP tool. min_score is now *float64 — omit → 0.4, explicit (incl 0) → passthrough. - cix_workspace_search: add the missing min_score parameter (the skill docs promise it and the agent errored without it). Thread it through client.WorkspaceSearch (server already supports ?min_score=); add a matching `cix ws search --min-score` flag for CLI parity. - formatWorkspaceSearch: surface status=="partial_failure" with a banner, the way the CLI renderSearch does, so partial results aren't read as complete. Add TestRegisterCixTools_SchemaReflection guarding the *float64 schema reflection. go test ./... green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cowork): tighten host-config perms & warn on empty partial_failure - mcp install/uninstall: preserve the host config's file mode and create a fresh one private (0o600) instead of 0o644 — it sits next to other MCP servers' secret env values; chmod even on overwrite so an already world-readable config is tightened rather than left as-is. - formatWorkspaceSearch: check partial_failure BEFORE the empty-result shortcut (like the CLI's renderSearch) so an empty result caused by repo failures warns about incomplete coverage instead of reading as "no matches". - test: cover the empty partial_failure case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * docs: credit odvcencio/gotreesitter in acknowledgements The Go tree-sitter binding cix's AST chunking first grew from. Credit where the head start came from. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: credit CLI and dashboard dependencies in acknowledgements The acknowledgements covered the server stack but omitted two whole categories. Add them: - CLI: Cobra, Charm (Bubble Tea/Bubbles/Lip Gloss), the MCP Go SDK (which the Cowork integration stands on), rjeczalik/notify, and koanf. - Dashboard: React + Vite, Radix UI + Tailwind (shadcn/ui pattern), TanStack Query, openapi-typescript, lucide, and sonner. Also point the closing note at server/dashboard/package.json for the full frontend dependency list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(server): read file + list directory endpoints for external projects (#120) * feat(server): read file + list directory endpoints for external projects Add two read-only, external-project-only capabilities so agents (Claude Code, and especially Cowork which has no local filesystem) can inspect the actual contents and file tree of workspace / external repos the harness cannot see locally: - POST /api/v1/projects/{path}/file — read a file whole or by line range - POST /api/v1/projects/{path}/tree — list a directory (one level, ls-like) Both read from the server-side git worktree (the source of truth; the index stores only chunks). Gated to EXTERNAL (GitHub-backed) projects via the existing access.IsProjectExternal helper — local projects return 409 with a hint to use native filesystem tools. Access is GroupRead (requireProjectAccess). Safety + concurrency: - safeJoin rejects path traversal (.., absolute, symlink escape) - new internal/repolocks package: per-repo RWMutex shared between the file read handlers (read-lock) and the clone worker's worktree rewrite (write-lock around CloneOrFetch), so a read never observes a mid-reset tree - size/line/entry caps with truncated flags Surfaced across all interfaces: - CLI: cix file <f> [--lines N:M], cix tree [dir] - MCP: cix_file, cix_tree (external-only, described) - skills: file/dir section in the three cix SKILL.md + cix-workspace notes, new /cix:file and /cix:tree plugin commands Tests: internal/httpapi/file_test.go covers gating (local 409, uncloned 409, unauthorized 404, group-shared 200), path traversal, line ranges, tree listing, and a required concurrency test (5 readers vs a non-atomic writer, green under -race). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(server): block .git via /file, honest empty-range signal, fail loud past byte cap Addresses review findings on the file/tree endpoints: - safeJoin now rejects the repo-root .git segment, so git metadata is unreadable via /file too (it was already hidden from /tree). First-segment check targets exactly the clone's own .git. - CLI and MCP renderers decide "no lines" on end_line < start_line (the server's authoritative signal) instead of Content == "", which wrongly swallowed a legitimate single-blank-line file. - ReadProjectFile returns 400 with an actionable detail when a line range begins past the 2 MiB read cap, instead of a confusing empty 200. Tests: .git paths added to the traversal test; new TestReadProjectFile_RangePastByteCap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(server): address PR #120 review — lock ordering, panic-safe locks, honest total_lines - file.go: take the per-repo read-lock BEFORE path validation so a concurrent clone can't swap in an escaping symlink between check and open (TOCTOU) - file.go: reject inverted line ranges (end<start) with 422 instead of silently clamping — enforced server-side for MCP/raw callers, not just the CLI - file.go: /tree now 400s on a malformed body instead of silently listing root - file.go: total_lines counts the whole file on disk (streams past the 2 MiB content cap) instead of just the buffered slice - repojobs: run the indexer's worktree walk under the per-repo read-lock so a concurrent clone reset can't tear the index across two revisions - repolocks: add panic-safe WithWrite/WithRead helpers (defer-unlock); the clone worker used a plain unlock that leaked the lock forever on a recovered panic - tests: symlink-swap TOCTOU guard, /tree access-gating, byte-cap total_lines, inverted-range 422, malformed-body 400, repolocks panic + mutual-exclusion, handleIndex-takes-read-lock, and a FileContent/DirectoryListing wire golden Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(cli): file/tree client + MCP compatibility tests; fix(cli): raw-mode truncation warning - cmd/file.go: emit the truncation warning to stderr in --raw mode too, so `cix file … --raw > copy` no longer silently drops capped content - client/file_test: parse the server's FileContent/DirectoryListing wire shape (twin golden of the server-side contract test), assert 422/409/404/400 surface as errors, and that an unset range omits start/end from the request body - cmd/mcp_tools_test: formatFileContent/formatTree rendering, incl. the empty-range (end<start) and truncated signals the MCP tools rely on - cmd/file_test: raw-mode truncation-warning coverage (stdout vs stderr) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * fix(skills): forward-port #120 file/dir docs to canonical + enforce sync in CI #120 added the `cix tree`/`cix file` guidance block to the plugin copy of the cix-workspace skill (and to skills/cix/SKILL.md and both cix-cowork skills) but missed the canonical skills/cix-workspace/SKILL.md — so sync-skills.sh --check drifted, and a plain `sync-skills.sh` run would have overwritten the plugin copy and deleted #120's addition. - Forward-port the block into canonical skills/cix-workspace/SKILL.md (plugin was ahead; copy plugin → canonical to restore byte-identity). - Align the investigator agent `description` quoting (canonical → quoted, matching the plugin bundle). - Wire sync-skills.sh --check into ci-plugin.yml and broaden its path triggers to plugins/** and skills/** so a canonical-only edit runs the drift check — previously only plugins/cix/** triggered it, the exact gap that let #120 drift through unnoticed. - Validate the cix-cowork plugin manifest in the jq step too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(server/cuda): bump pinned llama.cpp digest (#108) Upstream ghcr.io/ggml-org/llama.cpp:server-cuda moved past the pinned digest. Bump to the current digest reported by the weekly pin-freshness workflow: 2ea4be9… → d0cd0c4563e046f6ad0c8c27fe78280376d0d645cc3fef7a1ab87c75de08377b The runtime stage copies all /app/*.so* so a shared-lib layout change stays robust, but `make scout-cuda` must still rebuild + scan (and confirm /app/llama-server resolves its libs) before promoting to prod — release-server.yml runs that scan at tag time. Closes #108 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(dashboard): make legacy clipboard copy read its own textarea, not the key input On plain-HTTP deploys (e.g. http://<lan-ip>:21847) the API-key "created" popup runs in a non-secure context, so navigator.clipboard is unavailable and both Copy buttons fall through to the document.execCommand('copy') fallback (legacyCopy). That fallback was fragile on WebKit: the reveal screen auto-selects the "Full key" input on mount, and the copy textarea was hidden with opacity:0. WebKit refuses execCommand('copy') on effectively-invisible nodes, so the copy silently read the still-selected key input instead — which made the "Connect the cix CLI" button copy the bare key instead of the CLI command. Harden legacyCopy: - blur the previously focused/selected element so a stale selection can't win - position the textarea off-screen instead of opacity:0 (a real rendered node) - setSelectionRange after select() for iOS/older Safari Verified in Chromium that the copy source is the intended textarea for both buttons. Frontend-only; no CLI/server change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dvcdsys
added a commit
that referenced
this pull request
Jul 2, 2026
#120 added the `cix file` / `cix tree` slash commands and skill guidance to the cix plugin but left the version at 0.2.1. `claude plugin update` keys on the manifest version, so it sees 0.2.1 == 0.2.1 and silently no-ops — users on an older 0.2.1 never receive the new content unless they remove+reinstall. Bump to 0.3.0 so the new commands propagate via the normal update path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dvcdsys
added a commit
that referenced
this pull request
Jul 2, 2026
chore(plugin/cix): bump version 0.2.1 → 0.3.0 so plugin update propagates #120
This was referenced Jul 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two read-only, external-project-only endpoints (surfaced through server API → CLI → MCP → skills) that let an agent read the actual contents and file tree of a repo the harness can't see locally:
POST /api/v1/projects/{path}/file— read a file, whole or by line range (FileReadRequest{file,start?,end?}→FileContent)POST /api/v1/projects/{path}/tree— list a directory, one level, ls-like (TreeRequest{dir?}→DirectoryListing)Why
Workspace / external (GitHub-backed) projects are used for research, but their files live only on the cix server — the harness (Claude Code locally, and especially Cowork, which has no local filesystem) can't read them. Search returns only chunk fragments; there was no way to read a whole file, a line range, or browse a directory. The server-side git worktree is the source of truth (the index stores only chunks — full files aren't reconstructable from them), so these read straight from disk.
How
access.IsProjectExternal(git_repos-peer) helper — no new schema column. Local projects →409with adetailhint to use native filesystem tools; external-but-not-yet-cloned →409. Access level GroupRead (requireProjectAccess).safeJoinrejects.., absolute paths, and symlink escape (EvalSymlinks re-check within root).internal/repolockspackage — a per-reposync.RWMutexshared (same instance) between the file read handlers (read-lock) and the clone worker's worktree rewrite inrepojobs(write-lock aroundCloneOrFetch). A read never observes a mid-resetworktree. Both run in one process, so an in-process lock is sufficient.truncatedflags.{path}= project hash in the URL, POST + JSON body, file arg namedfile/dir(notpath, to avoid colliding with the{path}hash).Surfaced across interfaces: CLI
cix file <f> [--lines N:M]/cix tree [dir]; MCPcix_file/cix_tree(external-only in their descriptions); the threecixSKILL.md files +cix-workspacenotes +/cix:file/cix:treeplugin commands.Type of change
Checklist
go vet ./...passes (server + CLI)go test ./...green;make build+make openapi-checkpassTestReadProjectFile_Concurrent, 5 readers vs non-atomic writer) green under-raceFollow-up (separate PR)
Rename the misleading
{path}URL placeholder (it's really the project ID hash) →{projectId}. Non-breaking on the wire; deferred to keep this PR focused.🤖 Generated with Claude Code