Skip to content

feat: team rules from a linked Altimate workspace in Claude Code sessions - #13

Merged
saravmajestic merged 10 commits into
mainfrom
feat/workspace-rules
Nov 10, 2026
Merged

saravmajestic merged 10 commits into
mainfrom
feat/workspace-rules

Conversation

@saravmajestic

@saravmajestic saravmajestic commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Team rules from a linked Altimate workspace now apply in Claude Code sessions. This needs altimate-code with learn hook. That is AltimateAI/altimate-code#1626 (merged; it builds on AltimateAI/altimate-code#1605), with the rules state in the user's state directory from AltimateAI/altimate-code#1669 (merged, in v0.12.10). #1626 is not in a release yet (v0.12.10 does not have it): merge and release this after the first altimate-code release that includes #1626. With an older altimate-code the hooks do nothing: the session-start output is dropped unless it is the hook's JSON.

  • Workspace-rules hooks (SessionStart, UserPromptSubmit, Stop) call altimate-code learn hook <event> with the host's hook input on stdin.
    • Session start gives the agent the approved team rules, the number of proposals waiting, and the owner's reviews since the last session. The timeout is 15s, and the sync waits at most 3s.
    • Prompt submit records a correction with no model call.
    • Stop runs the post-turn check in the background: one drafting call per check round, and none while 20 proposals are waiting. With no model configured for altimate-code, nothing is sent.
    • Every event runs the CLI only in a linked folder: the folder the session starts in, or one above it up to the repository root, must be a key in altimate-code's link cache (~/.local/state/altimate-code/altimate-workspace-bindings.json, or under $XDG_STATE_HOME). The physical path is tried first, then the path the host used (a symlink). No file in the repository turns the hooks on.
    • Prompt submit and Stop run detached. Every path exits 0. ALTIMATE_LEARN=0 starts nothing.
  • /altimate-code:rules lists the stored proposals, the team rules and your proposals' status, asks with Claude Code's question tool, and answers through learn proposals reply.
  • Docs: README "Team rules", CHANGELOG, and PRIVACY.md (a "Workspace rules" section; Last revised November 10, 2026). It lists:
    • what the hooks hand the CLI (your messages and the transcript's location);
    • the digest sent to altimate-code's own model to word a proposal, with what it contains;
    • what reaches the workspace (only proposals you send, and usage counts);
    • the rules telemetry;
    • that with learning off the CLI still pulls rules and sends answers and usage counts.
  • Version 1.5.0 (main is at 1.4.1; this merges main's MCP mode, workspace skills, lineage and guard with the workspace-rules hooks, in one Stop list).

Test plan

  • bash tests/resolve-cli.sh (now covers commands/rules.md and learn calls)
  • bash tests/workspace-rules-hook.sh (new, stub CLI):
    • passes the input and host through;
    • prints only the CLI's JSON;
    • stays quiet when the CLI is missing, fails, or is too old;
    • runs the CLI only where the link cache names the folder or a parent within the repository (not for a planted rules.json, a prefix sibling, a value-only path, an empty key, or a parent above the repository through a symlink);
    • finds a link keyed by a symlinked path, a Windows native path, and escaped names;
    • per-turn events run in the background.
  • Live run in Claude Code against a linked workspace, after the altimate-code release

🤖 Generated with Claude Code

…ions

- Workspace-rules hooks (SessionStart, UserPromptSubmit, Stop) call `altimate-code learn hook <event>`.
  - Session start gives the agent the workspace's approved team rules, the number of proposals waiting, and new reviews.
  - Corrections are recorded with no model call.
  - The post-turn check runs in the background.
  - The hooks always exit 0, run the per-turn events only where workspace rules state exists, and print only the CLI's hook JSON.
- `/altimate-code:rules` answers stored proposals through `learn proposals reply`.
- Tests: a stub-CLI test for the hook script, and the resolver test covers the new command and `learn` calls.
- README, CHANGELOG and PRIVACY (what the hooks read and send), version 1.2.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread plugins/altimate-code/commands/rules.md Outdated
PROPOSAL_ID='<the proposal id>'
WORDING="$(cat <<'ALTIMATE_RULE'
<the new wording>
ALTIMATE_RULE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The documented heredoc cannot terminate as written

The sample indents the body and the closing ALTIMATE_RULE by three spaces, but a heredoc delimiter must start at column 1 (<<- only allows leading tabs). Line 51 then tells the model to "start the closing ALTIMATE_RULE line at the beginning of its line", so the sample and the instruction disagree. If the sample is copied, bash hits EOF with the heredoc still open and the learn proposals reply --edit=... call never runs (the edit flow just fails with a shell error). The same trap exists for the body line on 46: if it stays indented, the leading spaces become part of the new wording. commands/altimate.md (lines 31-36) shows the working form with ALTIMATE_TASK at column 0 — dedent lines 46-47 the same way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a6a01db: the block is flush left and says to run it exactly as shown.

Comment thread PRIVACY.md Outdated
| A proposed rule, with an excerpt of your correction if your workspace allows quotes | Your Altimate workspace's review queue, only when you send or approve it in `/altimate-code:rules` | Per the Altimate Privacy Policy |
| Rule usage counts (sessions a rule was shown in, corrections after it) | Your Altimate workspace | Per the Altimate Privacy Policy |

Turn this off with `altimate-code learn disable` in the project, `ALTIMATE_LEARN=0`, or the workspace's "Learn from sessions" setting.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Use altimate for the opt-out command

Every other instruction in this PR (README.md uses altimate learn enable, altimate learn status, altimate learn proposals) names the canonical altimate binary. Per CHANGELOG 1.1.1 and the resolver in the hooks, older curl and PowerShell installs expose only altimate, so altimate-code learn disable fails with "command not found" for those users — on the one line that tells them how to switch learning off.

Suggested change
Turn this off with `altimate-code learn disable` in the project, `ALTIMATE_LEARN=0`, or the workspace's "Learn from sessions" setting.
Turn this off with `altimate learn disable` in the project, `ALTIMATE_LEARN=0`, or the workspace's "Learn from sessions" setting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a6a01db: altimate learn … throughout.


case "$EVENT" in
session-start)
OUTPUT="$(printf '%s' "$INPUT" | "$ALTIMATE_BIN" learn hook session-start --host "$HOST" 2>/dev/null)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Session start invokes the CLI in every project, contrary to the stated gating

This call is unconditional: every Claude Code session start in every repository spawns altimate-code and waits for its bounded sync (up to 3s, capped by the 15s hook timeout). That may be the only way to discover whether the project is linked, but it contradicts the header comment above ("so other projects pay nothing") and CHANGELOG line 11 ("The hooks run only in a linked project with learning on"). Worth either adding a cheap local gate (e.g. skip when .altimate-code/ is absent, plus a path for the first session after linking) or rewording both claims so users are not told unrelated projects cost nothing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed (the link-cache gate); refined in a6a01db to the exact walk the CLI does.

@kilo-code-bot

kilo-code-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
plugins/altimate-code/hooks-handlers/workspace-rules.sh 60 New: the second (symlink) walk stops only when a folder's physical path equals the repository root, so when the symlink sits outside the repo and points at a subfolder, the walk climbs the host's ancestors outside the repository and can match an unrelated link (e.g. ~/projects), starting the CLI in an unlinked project. The empty-key guard from the previous review is fixed.
Files Reviewed (3 files in the incremental commit)
  • PRIVACY.md - 0 issues (the change note now names plugin 1.5.0 on November 10, matching plugin.json, CHANGELOG 1.5.0 and "Last revised")
  • plugins/altimate-code/hooks-handlers/workspace-rules.sh - 1 issue (the empty cygpath key no longer matches, and the physical-then-logical walk finds symlink-keyed caches, but the logical walk is not bounded to the repository when the symlink enters a subfolder)
  • tests/workspace-rules-hook.sh - 0 issues (empty-key and symlinked-key cases are asserted; there is no fixture for a symlink that enters a subfolder from outside, which is why the bound above is untested)

The previously reported empty-key suggestion is resolved. Behaviour against a real altimate-code release still cannot be verified in this sandbox; the link-cache filename/JSON shape and the learn hook contract are taken from the PR description and the test stub.

Previous Review Summaries (4 snapshots, latest commit a6a01db)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit a6a01db)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
plugins/altimate-code/hooks-handlers/workspace-rules.sh 57 If cygpath -w is present but returns an empty value, the escaped key is empty and the grep searches for "", which can match any empty JSON string in the link cache, so session start runs the CLI in a project with no workspace link.
Files Reviewed (6 files in the incremental commit)
  • PRIVACY.md - 0 issues (workspace-rules section rewritten; the opt-out now names altimate learn disable, matching the rest of the docs)
  • plugins/altimate-code/CHANGELOG.md - 0 issues (session-start gating and ALTIMATE_LEARN wording match the hook)
  • plugins/altimate-code/README.md - 0 issues (subfolder-link, new-clone and per-turn-hook wording match the code)
  • plugins/altimate-code/commands/rules.md - 0 issues (the edit sample is now flush left and the ok/code branches are consistent)
  • plugins/altimate-code/hooks-handlers/workspace-rules.sh - 1 issue (the new linked() walk correctly mirrors the CLI and rejects sibling prefixes; only the empty-key guard remains)
  • tests/workspace-rules-hook.sh - 0 issues (hooks.json parsing, no-link, subfolder, sibling-prefix, quote/backslash and native-path cases, ALTIMATE_LEARN, and background timing are all asserted)

Behaviour against a real altimate-code release still cannot be verified in this sandbox; the link-cache filename/JSON shape and the learn hook contract are taken from the PR description and the test stub.

Previous review (commit 6e87e8f)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
plugins/altimate-code/hooks-handlers/workspace-rules.sh 41 New: the grep -qF "\"$REAL" link-cache prefix match also matches sibling paths (/work/app-old for /work/app), so the CLI starts in unlinked projects despite the "not started at all" comment.
plugins/altimate-code/commands/rules.md 47 Carried: the heredoc sample indents the body and the closing ALTIMATE_RULE by three spaces, so a copied command never terminates; line 51 also tells the model to start the delimiter at column 1. Unchanged in 6e87e8f.

SUGGESTION

File Line Issue
plugins/altimate-code/hooks-handlers/workspace-rules.sh 39 New: altimate-workspace-bindings.json is an undocumented, single way to discover a link; since session start now creates the only state that enables the per-turn hooks, any cache path/format/symlink mismatch silently disables the whole feature with no diagnostic.
PRIVACY.md 36 Carried: the opt-out is documented as altimate-code learn disable, but curl/PowerShell installs expose only altimate (all other docs use altimate learn …). Unchanged in 6e87e8f.
Files Reviewed (5 files, 4 in the incremental commit)
  • plugins/altimate-code/README.md - 0 issues (the new "needs the words team rule" wording is consistent with the command docs)
  • plugins/altimate-code/commands/rules.md - 1 carried issue
  • plugins/altimate-code/hooks-handlers/workspace-rules.sh - 2 new issues; the previous session-start gating suggestion is resolved by the added state/link-cache gate
  • tests/workspace-rules-hook.sh - 0 issues (the new no-link and subfolder-link cases assert the gate correctly)
  • PRIVACY.md - 1 carried issue (targeted re-check only)

Behaviour against a real altimate-code release still cannot be verified in this sandbox; the cache filename/JSON shape and the learn hook contract are taken from the PR description and the test stub.

Previous review (commit 4899cf8)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
plugins/altimate-code/commands/rules.md 47 Heredoc sample indents the closing ALTIMATE_RULE and the wording body, contradicting the instruction on line 51; copied verbatim the heredoc never terminates and the reply --edit command fails.
PRIVACY.md 36 Opt-out documented as altimate-code learn disable, but curl/PowerShell installs expose only altimate, so the command fails with "command not found" for those users.

SUGGESTION

File Line Issue
plugins/altimate-code/hooks-handlers/workspace-rules.sh 31 Session start calls the CLI in every project (up to a 3s sync), contradicting the "other projects pay nothing" comment and CHANGELOG line 11.
Files Reviewed (10 files, 3 in the incremental commit)
  • .claude-plugin/marketplace.json - 0 issues
  • PRIVACY.md - 1 issue
  • plugins/altimate-code/.claude-plugin/plugin.json - 0 issues
  • plugins/altimate-code/CHANGELOG.md - 0 issues (inaccuracy noted in the hook comment)
  • plugins/altimate-code/README.md - 0 new issues
  • plugins/altimate-code/commands/rules.md - 1 issue
  • plugins/altimate-code/hooks-handlers/workspace-rules.sh - 1 issue
  • plugins/altimate-code/hooks/hooks.json - 0 issues
  • tests/resolve-cli.sh - 0 issues
  • tests/workspace-rules-hook.sh - 0 issues

Incremental commit 4899cf8 only touched docs (CHANGELOG, README, rules.md). The new "remember" wording and the added --edit/not_available guidance are consistent with the hook comments and PRIVACY.md. The three findings above are re-verified against current HEAD and remain unresolved. Behaviour against a real altimate-code release still cannot be verified in this sandbox (no learn hook CLI), so the CLI contract (--json, --edit=, --approve-anyway, ALTIMATE_LEARN=0, git-root state resolution) is assumed from the PR description.

Fix these issues in Kilo Cloud

Previous review (commit d95762e)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
plugins/altimate-code/commands/rules.md 47 Heredoc sample indents the closing ALTIMATE_RULE and the wording body, contradicting the instruction on line 51; copied verbatim the heredoc never terminates and the reply --edit command fails.
PRIVACY.md 36 Opt-out documented as altimate-code learn disable, but curl/PowerShell installs expose only altimate, so the command fails with "command not found" for those users.

SUGGESTION

File Line Issue
plugins/altimate-code/hooks-handlers/workspace-rules.sh 31 Session start calls the CLI in every project (up to a 3s sync), contradicting the "other projects pay nothing" comment and CHANGELOG line 11.
Files Reviewed (10 files)
  • .claude-plugin/marketplace.json - 0 issues
  • PRIVACY.md - 1 issue
  • plugins/altimate-code/.claude-plugin/plugin.json - 0 issues
  • plugins/altimate-code/CHANGELOG.md - 0 issues (inaccuracy noted in the hook comment)
  • plugins/altimate-code/README.md - 0 issues
  • plugins/altimate-code/commands/rules.md - 1 issue
  • plugins/altimate-code/hooks-handlers/workspace-rules.sh - 1 issue
  • plugins/altimate-code/hooks/hooks.json - 0 issues
  • tests/resolve-cli.sh - 0 issues
  • tests/workspace-rules-hook.sh - 0 issues

Notes: the hook script's resolver block matches the existing copies byte-for-byte and rules.md carries the expected 2 blocks, so the resolver test should stay green. Behaviour against a real altimate-code release could not be verified in this sandbox (no learn hook CLI available), so the CLI contract (--json, --edit=, --approve-anyway, ALTIMATE_LEARN=0, git-root state resolution) is assumed from the PR description and the other repo.

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4.1-flash-1017 · Input: 93.9K · Output: 34K · Cached: 970.5K

saravmajestic and others added 2 commits October 30, 2026 12:05
…terrupted answer

- README and CHANGELOG: "Remember this as a team rule: …" said to the agent waits as a proposal, and rule drafting needs altimate-code's own model.
- `/altimate-code:rules`: handles an answer that stopped part-way (`not_available` … "was being answered with").
- Codex: the sandbox has no network, so a proposal sent from `/altimate-code:rules` is queued until the next turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Session start runs the CLI only where workspace rules state exists, or where the workspace link cache names the repository or a folder inside it. Elsewhere the hook costs nothing.
- The per-turn hooks share the same state check. The CLI still runs from the host's directory: a link can be for a folder inside the repository.
- README: direct "remember" needs "team rule" wording. `/altimate-code:rules`: a here-doc delimiter note.
- Hook test covers the pre-filter and a link for a folder inside the repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
if [ ! -f "$STATE" ]; then
CACHE="${XDG_STATE_HOME:-$HOME/.local/state}/altimate-code/altimate-workspace-bindings.json"
REAL="$(cd "$ROOT" 2>/dev/null && pwd -P)"
[ -n "$REAL" ] && grep -qF "\"$REAL" "$CACHE" 2>/dev/null || exit 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: The prefix match treats sibling directories as linked

grep -qF "\"$REAL" matches any cache key that starts with the repository path, so a repo at /work/app also matches a binding for /work/app-old/dbt or /work/application, and the CLI is started in a project with no workspace link — contrary to the comment above ("elsewhere the CLI is not started at all"). Match only the exact key or a child key, for example:

{ grep -qF "\"$REAL\"" "$CACHE" || grep -qF "\"$REAL/" "$CACHE"; } 2>/dev/null || exit 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a6a01db: exact key with both quotes. Sibling-prefix negative test added.

# Only a repository with workspace rules state, or one the workspace link cache names (the repository or a folder
# inside it), can have team rules: elsewhere the CLI is not started at all.
if [ ! -f "$STATE" ]; then
CACHE="${XDG_STATE_HOME:-$HOME/.local/state}/altimate-code/altimate-workspace-bindings.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The link cache is a single silent gate on the whole feature

Nothing else in the repository references altimate-workspace-bindings.json, and the test writes that exact path and JSON shape itself (tests/workspace-rules-hook.sh:52), so it cannot detect a mismatch with the real CLI. Because session start is now the only place that creates rules.json, any difference — a different state directory or filename, a bindings key that is not the folder path, or a symlinked project path (resolved by pwd -P here but stored unresolved by the CLI) — turns the hook into a permanent silent no-op: no rules at session start, no state file, so the per-turn hooks never run either, while README troubleshooting points at altimate learn status with no hint that the hook never ran. Consider a fallback (for example also run when the project has a .altimate-code/ directory) or documenting the cache contract in the repo.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly. The cache file is now named in PRIVACY.md and the hook comment documents its key format; its location is fixed by the CLI (xdg-basedir, every platform). A CLI-side rename would still need a matching plugin change, so it's noted as a contract in the hook comment.

@hkaltai hkaltai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peer review: APPROVE (with minor comments)

Reviewers: lead (Claude), Claude (fresh claude -p session), Codex (gpt-6.1-sol), with one convergence round.

The hook wrapper is defensive: every path exits 0, only the CLI's JSON is passed through, per-turn calls are detached with every file descriptor closed, and repositories without rules state or a link start no CLI. The /altimate-code:rules flow offers at most 4 choices. Nothing blocks merge here. The minor comments are inline, and the PRIVACY.md wording should be settled before release.

Dependency: merge and release after AltimateAI/altimate-code#1626, which has an open MAJOR. Without a configured model, the background Stop check auto-registers for Altimate Base and sends the digest with no disclosure. That affects what this PRIVACY.md can promise.

A linked-worktree root mismatch was raised, then withdrawn in convergence: the CLI's Instance.worktree is the --show-toplevel checkout, which matches $ROOT here.

Nits (consolidated)

  • plugins/altimate-code/hooks-handlers/workspace-rules.sh:29-30: the comment "Run from the host's directory, not the repository root" sits right above the line that computes the repository root, so it reads as contradicting it. Suggest: "$ROOT only locates the state file; the CLI runs in the host's directory (no cd), because a link can be for a folder inside the repository."
  • plugins/altimate-code/hooks-handlers/workspace-rules.sh:41: grep -qF "\"$REAL" doesn't JSON-escape the path (a directory containing " or \ never matches) and is a prefix match (/x/app matches /x/app-old). The walk-up fix in the inline comment covers both.
  • Docs mix altimate learn … and altimate-code learn … for the same commands (README and PRIVACY). Pick one.
  • tests/workspace-rules-hook.sh: the slow stub's background processes outlive the test and are still sleeping when the trap deletes $WORK. Kill them before cleanup.
  • README: a new clone or worktree gets no team rules until altimate-code has run (or been linked) in that checkout, because both the gate and the CLI read only the directory-keyed bindings cache. Worth one line.

Scope vs ticket: cannot verify, because there's no ticket reference. Unrelated files: clean. Naming: clean, apart from the mixed command names above.

🤖 Generated with Claude Code

Comment on lines +38 to +42
if [ ! -f "$STATE" ]; then
CACHE="${XDG_STATE_HOME:-$HOME/.local/state}/altimate-code/altimate-workspace-bindings.json"
REAL="$(cd "$ROOT" 2>/dev/null && pwd -P)"
[ -n "$REAL" ] && grep -qF "\"$REAL" "$CACHE" 2>/dev/null || exit 0
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR — The gate lets through a child-folder link that the CLI can't find from the repo root

if [ ! -f "$STATE" ]; then
  CACHE="${XDG_STATE_HOME:-$HOME/.local/state}/altimate-code/altimate-workspace-bindings.json"
  REAL="$(cd "$ROOT" 2>/dev/null && pwd -P)"
  [ -n "$REAL" ] && grep -qF "\"$REAL" "$CACHE" 2>/dev/null || exit 0
fi

When only dbt/ is linked and the session starts at the repository root, this check passes (the key starts with $REAL). But the CLI's RulesService.linked() (altimate-code rules/service.ts:59-67) only walks up from the cwd to the root, so it returns not linked. The CLI starts, waits up to the first-pull budget, and gives no rules. The comment ("or a folder inside it") and the test at tests/workspace-rules-hook.sh:50 ("started from the repository root: the CLI runs") pass only because the stub always answers.

Suggested fix: mirror the CLI. Walk from pwd -P up to $ROOT, matching "\"$DIR\"" with the closing quote at each step; that also removes the /x/app vs /x/app-old prefix overmatch. Or teach the CLI to look below the cwd. Then change the test to start inside project/dbt/models, and add a negative case for a root start with only a child folder linked.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a6a01db. linked() walks from pwd -P up to the repository root and matches each folder as an exact, JSON-escaped key (cygpath -w on Windows), as the CLI does. Tests: start in project/dbt/models with dbt linked runs the CLI; root start with only dbt linked, and a project-old sibling, don't.

Comment thread PRIVACY.md Outdated

## Workspace rules

These hooks act only in a project linked to an Altimate workspace, with learning on, workspaces on and you signed in to Altimate. Otherwise the session-start hook reads only local altimate-code state and sends nothing, and the per-turn hooks do not run the CLI at all in a project without workspace rules state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR — The learning-off guarantee is stronger than the code

These hooks act only in a project linked to an Altimate workspace, with learning on, workspaces on and you signed in to Altimate. Otherwise the session-start hook reads only local altimate-code state and sends nothing, ...

RulesHost.start() checks rulesEnabled() (the learn switch and workspaces) before the pull. But the workspace's "Learn from sessions" setting and capture (learn disable sets capture=false) are checked only afterwards, in on(). So with either opt-out from line 36, a signed-in, linked project can still pull from Altimate at session start. Also, the per-turn hooks pass every prompt and the transcript path to a local CLI process wherever rules.json exists, even after learn disable, sign-out or unlink. The CLI then drops them, so nothing leaves the machine, but the wording implies the hooks stop.

Separately, the digest goes to "your configured LLM provider" only if one is configured. Otherwise the CLI currently auto-registers Altimate Base (see the MAJOR on AltimateAI/altimate-code#1626).

Suggested fix: narrow the wording to what is gated: capture and drafting are off; a rules pull still happens unless ALTIMATE_LEARN=0. Or change the CLI to check the workspace setting before pulling. Optionally, skip the per-turn hooks early on ALTIMATE_LEARN=0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a6a01db, and in the CLI (8464df4024 on altimate-code#1626). Team rules are now given with learning off, as in an altimate-code session; capture and drafting need learning on. PRIVACY.md says exactly that, says the per-turn hand-off is dropped locally when off, and the hooks start nothing with ALTIMATE_LEARN=0. The no-model case no longer registers anything.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at this head with independent Claude and Codex reviewers: partially fixed. Delivery and capture are now separated correctly, but the last sentence of line 33 is still stronger than the CLI's behaviour:

When learning is off, you are signed out or the project is not linked, the CLI discards them and nothing leaves your machine.

With learning off in a signed-in, linked project, RulesHost.check() still calls syncAfterCheck() (altimate-code rules/host.ts:351-355). That can send queued proposal replies and usage counts and pull rules at the next Stop. The CLI's own test says so: "with learning from sessions off, a queued reply is still sent at the next Stop".

Suggested fix: narrow it to what's true. With learning off, your messages and the transcript are discarded locally. Answers you already queued, the rules sync and usage counts can still be sent. Only ALTIMATE_LEARN=0 stops everything. (MINOR, not blocking.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cd8eede (codex plugin: a0b0581). PRIVACY now says that with learning off the CLI discards your messages and transcript, but after each turn it still pulls team rules and sends proposals you already answered and rule usage counts (no message or transcript text). Only ALTIMATE_LEARN=0 starts nothing.

Comment thread PRIVACY.md Outdated

- The plugin itself collects **no data**. It is a routing layer (a skill, a slash command, and a static session hook) that tells Claude Code when to delegate a task to the altimate-code CLI installed on your machine.
- The plugin itself collects **no data**. It is a routing layer (skills, slash commands, and session hooks) that tells Claude Code when to delegate a task to the altimate-code CLI installed on your machine.
- In a project linked to an Altimate workspace with learning on, its workspace-rules hooks hand your messages and the session transcript to the local altimate-code CLI, which keeps corrections on your machine and may send a digest to your LLM provider to word a proposed team rule. A proposal reaches your workspace only when you send or approve it. See [Workspace rules](#workspace-rules).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR — Effective date (line 3) unchanged after a material change, anchored here because line 3 isn't in the diff

**Effective date:** July 9, 2026

The policy now covers every prompt and the transcript, plus a digest sent to an LLM provider. The policy itself promises to revise the effective date when practices change. Set it to the 1.2.0 release date, and get the privacy sign-off the PR description asks for.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a6a01db: "Last revised: November 4, 2026" plus a dated note under Changes to this policy. Legal sign-off is still pending.

Comment thread tests/workspace-rules-hook.sh Outdated
cd "$(dirname "$0")/.."
HOOK="$(find "$PWD/plugins/altimate-code" -name '*workspace-rules.sh' | head -n1)"
[ -f "$HOOK" ] || { echo "FAIL: no workspace-rules hook script"; exit 1; }
grep -q "$(basename "$HOOK")" plugins/altimate-code/hooks/hooks.json || { echo "FAIL: hooks.json does not run $(basename "$HOOK")"; exit 1; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR — hooks.json is only checked by grepping for the script name

grep -q "$(basename "$HOOK")" plugins/altimate-code/hooks/hooks.json || { echo "FAIL: hooks.json does not run $(basename "$HOOK")"; exit 1; }

A typo in an event name (UserPromptSubmit/Stop), an argument (user-prompt-submit, which the script's case silently ignores), or a timeout would still pass. Parse it with python3 -c 'import json…' or jq, and assert each event runs the script with its matching argument and the 15/5/5 timeouts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a6a01db: hooks.json is parsed with python3, checking each event's argument and its 15/5/5 timeout.

@ralphstodomingo ralphstodomingo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes to PRIVACY.md, which the description asks to have reviewed. The code reads fine. These are the places where the policy doesn't match what altimate-code does:

  1. What the digest contains. The "session transcript" row says "a bounded, redacted digest". The digest altimate-code builds (packages/opencode/src/altimate/learn/digest.ts in altimate-code) holds the session's prompts, every tool call's input and output (up to 300 and 400 characters per call, so file contents, command output and warehouse rows), and the final reply, up to 24,000 characters in all. Redaction masks secrets only, not other data. The policy should say so.
  2. Where it goes. "Your configured LLM provider" reads as Claude Code's provider (Anthropic). It's actually altimate-code's own model: learn.model, else altimate-code's default (the Altimate LLM Gateway, or the provider configured for altimate-code). Name it, since for a Claude Code user it's a party that doesn't otherwise see the session.
  3. Telemetry. "does not phone home and does not collect analytics" is true of the plugin itself, but the CLI the hooks start sends rules telemetry: rules_sync, rules_delivered, rule_drafted and rule_proposal_shown (the last with the host session id). Add a row to the table, or point this section at "Usage telemetry" below.
  4. Who turns learning on. "with learning on" doesn't say that, in a linked project, the workspace's "Learn from sessions" setting turns capture on for a member who hasn't made a choice of their own. The policy lists that setting as an off switch; it should also say the workspace owner can switch capture on with it.
  5. Effective date. The data flows changed, and "Changes to this policy" says the date is revised when practices change.

Smaller:

  • In the description, stacked on #1605 links to this repository's own number 1605, not altimate-code's. Use AltimateAI/altimate-code#1605.
  • On Windows the session-start check of the link cache never matches. It greps for the Git Bash path (/c/Users/...), but altimate-code keys the cache by the native path, JSON-escaped (C:\\Users\\...). So on Windows, session start only runs the CLI where workspace-rules state already exists.
  • The edit here-doc in commands/rules.md is indented inside its list item. Copied as written, the closing delimiter doesn't end it, or the wording picks up leading spaces (Kilo's thread). Showing the block flush left avoids both.

As the description says, this must merge and ship after an altimate-code release that has learn hook, and also after the changes requested on AltimateAI/altimate-code#1626.

Appendix: complexity delta

No TypeScript or Python in this diff (a shell hook, Markdown and JSON), so there's nothing to measure.

- `hooks-handlers/workspace-rules.sh`: at session start, walk from the host's directory up to
  the repository root and match each folder as an exact JSON key of the
  link cache (escaped, native form via `cygpath -w` on Windows), as the
  CLI does. A link for a subfolder no longer starts the CLI from the
  repository root, and `/x/app` no longer matches `/x/app-old`.
- Skip every event with `ALTIMATE_LEARN=0` (or `false`).
- Correct the comments on `$ROOT`, blocking and model calls per `Stop`.
- `tests/workspace-rules-hook.sh`: parse `hooks.json` and check each
  event's argument and timeout; cover a start below a linked subfolder,
  a root start with only a subfolder linked, a sibling-prefix link,
  quote/backslash paths, Windows native keys and `ALTIMATE_LEARN`;
  replace the fixed sleep with a sentinel; stop the slow stub on exit.
- `commands/rules.md`: show the edit here-doc flush left, branch on
  `code` when `ok` is false and on `closed` when it is true, and never
  repeat `--approve-anyway` without fresh confirmation.
- `PRIVACY.md`: state what is gated by what, who can turn capture on,
  the digest's contents and caps, that it goes to altimate-code's own
  model rather than Claude Code's, and the rules telemetry the CLI sends; add a
  revision date.
- `README.md`, `CHANGELOG.md`: `altimate learn …` throughout, bounded
  session start, start in the linked folder, new clones and worktrees,
  where proposals are answered, and the corrected gating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
key="$dir"
command -v cygpath >/dev/null 2>&1 && key="$(cygpath -w "$dir")"
key="$(printf '%s' "$key" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
grep -qF "\"$key\"" "$cache" 2>/dev/null && return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Guard the empty key before matching the link cache

If cygpath is on PATH but fails or prints nothing for the directory (an incompatible shim, a path it cannot convert), key becomes empty and this grep searches for two quotes, i.e. an empty JSON string. Any empty string in the cache then makes linked return success, so session start runs the CLI in a project with no workspace link, contrary to the header comment and PRIVACY.md. Add a non-empty guard, for example prefixing the grep with [ -n "$key" ] &&.

Defensive only: with a working MSYS/Cygwin cygpath, converting a real cwd does not fail.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 390e882 (codex plugin: 5ef887e). An empty key returns "not linked" before the grep. Test: a cygpath that prints nothing, with an empty-string key in the cache, starts nothing. It fails on the old script.

@saravmajestic

Copy link
Copy Markdown
Contributor Author

Review round addressed in a6a01db. Each inline thread has a reply.

PRIVACY.md, for ralphstodomingo's points 1–5 and hkaltai's comments:

  • the digest's contents and caps, and that only secrets are redacted;
  • that it goes to altimate-code's own model (learn.model, or altimate-code's default), not the host's, and that nothing is sent when no model is configured;
  • the rules telemetry events, including the host session id;
  • that the workspace owner's "Learn from sessions" setting can turn capture on for members with no choice of their own;
  • what is gated by what, including that team rules are pulled whatever the learning setting;
  • a "Last revised" date.

Legal sign-off is still pending.

Smaller points:

  • the #1605 link now points to altimate-code;
  • the link check uses Windows native, JSON-escaped keys (cygpath -w);
  • the here-doc is flush left.

hkaltai's nits:

  • the $ROOT comment is fixed;
  • link keys are matched exactly and escaped;
  • altimate learn … is used throughout;
  • the slow stub is killed on exit;
  • the README now covers new clones and worktrees.

Dependency: merge and release after altimate-code#1626 ships in a release; the no-model and gating fixes are there.

🤖 Generated with Claude Code

Every hook event now starts altimate-code only where altimate-code's
workspace link cache (in the person's own state directory) names the
session's folder or a folder above it in the repository. The per-turn hooks
no longer run because `.altimate-code/learn/workspace-rules/rules.json`
exists: a file in the repository can come from a branch or a merge, and
altimate-code no longer keeps its rules state there.

- Hook script: one gate (`linked`) for every event; `$STATE` removed.
- Tests: a rules state file planted in the repository starts nothing, at
  session start or per turn; the per-turn checks use the link cache.
- PRIVACY.md, README, CHANGELOG: the hooks run only in a linked folder;
  the rules state, the proposals waiting to be sent and review notices are
  kept in the person's own state directory, outside the project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kilo-code-bot

kilo-code-bot Bot commented Nov 5, 2026

Copy link
Copy Markdown

Kilo Code Review could not run — your account is out of credits.

Add credits or switch to a free model to enable reviews on this change.

@hkaltai hkaltai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peer review: REQUEST CHANGES (docs only)

Reviewers: lead (Claude), Claude (fresh claude -p session) and Codex (gpt-6.1-sol), as independent reviewers, with one convergence round. This PR was reviewed together with its sibling for parity, against the altimate-code#1626 CLI at b7f42b63.

The hook code is in good shape:

  • The calls match the CLI: learn hook <event> --host claude, the input passed on stdin, and the output passed through only when it's the hook's JSON.
  • Every path exits 0, and per-turn events run detached.
  • The link-cache walk matches RulesService.linked(): physical paths (pwd -P against keys canonicalized with realpathSync), stopping at the repository root, segment-safe matching, JSON escaping and cygpath on Windows.
  • The newest commit means a file brought in by a branch can no longer start the CLI.

Both hook scripts are identical apart from HOST and the order of CLI names, which follows each repo's convention.

Blocking: PRIVACY.md:39 states where data is kept, and it's wrong (inline). The privacy wording was the reason for the last request-changes, so it should be accurate before release. Three MINOR findings are inline: the telemetry and model disclosure, the link-cache over-match, and no test run.

Prior findings

Resolved, checked in the code:

  • the $ROOT comment, escaping and prefix matching, the Windows path, the flush-left here-doc, the slow stub cleanup, and the new-clone line in the README;
  • the hooks.json parse test, the digest contents and destination, who turns learning on, and the dated revision.
  • ralph's telemetry point is partly fixed (inline on L44).
  • The PR-description link to #1605 and the reply-only threads couldn't be verified from the checkout.

Nits (consolidated)

  • The hook header (lines 4-6) uses "learning" for two different switches. ALTIMATE_LEARN/learn.enabled gates delivery, while capture also needs the workspace's "Learn from sessions" setting. Say which is which.
  • The CONTRIBUTING.md layout still describes only the SessionStart hook. Add the workspace-rules script, the three events and the new test.

Not verified

The test scripts were read, not run. Nothing checks whether Claude Code kills the hook's process group, which would also end the detached per-turn CLI. No fixture pins the link-cache format the hooks depend on.

Note for altimate-code#1626 (not this PR): the CLI's own RulesService.linked() still counts a repository rules.json that has scope as linked.

Scope vs ticket: cannot verify, because there's no ticket reference. Unrelated files: clean.

🤖 Generated with Claude Code

Comment thread PRIVACY.md Outdated

| Data | Where it goes | Retention |
|---|---|---|
| Your workspace's approved team rules | Pulled by the local CLI from your Altimate workspace, given to Claude Code as session context | Mirrored locally in the project's git-ignored `.altimate-code/` directory, and given only while altimate-code's own rules state (in your state directory, `~/.local/state/altimate-code/`, outside the project) holds the same rules. That state also keeps proposals waiting to be sent and your review notices |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAJOR (privacy policy): the policy says rules state is kept outside the project, but it's in the repository

…and given only while altimate-code's own rules state (in your state directory, ~/.local/state/altimate-code/, outside the project) holds the same rules. That state also keeps proposals waiting to be sent and your review notices

The CLI in altimate-code#1626 keeps this state in <repo>/.altimate-code/learn/workspace-rules/rules.json:

  • rules/store.ts:259-261 puts the file under Store.paths(root, …).learnDir, which is <root>/.altimate-code/learn/<name>.
  • Nothing under altimate/rules/ uses Global.Path.state.

Only the workspace link cache (altimate-workspace-bindings.json) lives in ~/.local/state/altimate-code/. Someone who trusts the policy will think the proposal text and review notices are outside the checkout. A git add -f, an archive or a copy of the project includes them.

Suggested fix: say the rules state, proposals waiting to be sent and review notices are kept in the project's git-ignored .altimate-code/learn/workspace-rules/ folder. Mention the user state directory only for the link cache. The "given only while…" clause could become "given only while you're signed in as the account they were pulled for", which is what delivering() checks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fb70ac1. The wording now matches the CLI from altimate-code#1669: the rules state, proposals waiting to be sent and review notices are in the user state directory ($XDG_STATE_HOME/altimate-code/workspace-rules/, usually ~/.local/state/...), keyed by the project's real path, and the project's copy of the rules is given only while that state holds the same rules and you are signed in as the account that pulled them (#1669, b600a0b5b6). You were right that at the reviewed head (#1626 at b7f42b63) the state was still in the repository. #1626 rebases onto #1669 and this plugin ships after a release that includes it. The row also says that earlier releases keep the state in the project's .altimate-code/learn/workspace-rules/ folder.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at this head: the wording is now conditional ("This needs an altimate-code release that keeps the rules state there"), which is fine. But nothing gates the release on that CLI change:

  • The PR description still says "Merge and release after #1626 ships". The state move is fix: workspace rules state lives outside the project altimate-code#1669, which is still OPEN.
  • The description also still says the per-turn hooks run "only where .altimate-code/learn/workspace-rules/rules.json exists". The code now gates every event on the user-level link cache, and the test asserts that a rules.json in the repository does not start the CLI.

Suggested fix: make the gate "after an altimate-code release that includes #1626 and #1669", name that minimum altimate-code version in the README/CHANGELOG, and update the hook-gating bullet. The privacy reviewer will read the description. (MINOR, not blocking.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated both descriptions. #1626 and #1669 are merged. #1669 is in v0.12.10 and #1626 is not released yet, so the merge gate is now "the first altimate-code release that includes #1626". The gating bullet now describes the link-cache gate rather than rules.json.

Comment thread PRIVACY.md Outdated
| The session transcript, after a turn with new corrections (learning on) | Read locally by the CLI; the digest described above goes to altimate-code's own model to word one proposed rule | The transcript stays local; the model provider's handling is as for delegated tasks below |
| A proposed rule, with an excerpt of your correction if your workspace allows quotes | Your Altimate workspace's review queue, only when you send or approve it in `/altimate-code:rules` or `altimate learn proposals` | Per the Altimate Privacy Policy |
| Rule usage counts (sessions a rule was shown in, corrections after it) | Your Altimate workspace | Per the Altimate Privacy Policy |
| Rules telemetry events: `rules_sync`, `rules_delivered`, `rule_drafted`, `rule_proposal_shown` (with the Claude Code session id, as `claude:<id>`), `rule_proposal_answered`, `rule_proposal_skipped`, `rule_remember_routed`. No rule text, prompts or file paths | Altimate's usage telemetry, as described in [Usage telemetry](#usage-telemetry) | As for usage telemetry; turned off with the telemetry opt-out there |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR (privacy policy): the telemetry and model disclosures are incomplete

Rules telemetry events: rules_sync, rules_delivered, rule_drafted, rule_proposal_shown (with the Claude Code session id, as claude:<id>), …

  • rules_command is missing. /altimate-code:rules runs learn proposals and learn proposals reply, and the learn middleware sends rules_command for every subcommand except hook (learn.ts:1445-1450 in #1626).
  • The session id isn't specific to one event. Every rules event carries session_id, and host-driven events use claude:<id>. Putting it next to rule_proposal_shown only is ambiguous.
  • ALTIMATE_LEARN_MODEL is missing. On line 35, learnModel() prefers ALTIMATE_LEARN_MODEL over learn.model (learn/auto.ts:38-40). Someone who set the variable is told the digest goes elsewhere.

Suggested fix: add rules_command, say the rules events carry the session id, and write "ALTIMATE_LEARN_MODEL, else learn.model, else …".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fb70ac1: rules_command is listed (the learn subcommand name; not sent for the hooks' own learn hook calls), the row says events about a session carry its id (claude:<id> for this host's sessions; rules_sync and rules_command carry none), and the model line reads ALTIMATE_LEARN_MODEL, otherwise learn.model, otherwise altimate-code's default model.

key="$dir"
command -v cygpath >/dev/null 2>&1 && key="$(cygpath -w "$dir")"
key="$(printf '%s' "$key" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
grep -qF "\"$key\"" "$cache" 2>/dev/null && return 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR: the link-cache check matches JSON values as well as keys

    grep -qF "\"$key\"" "$cache" 2>/dev/null && return 0

Each cache row also has a projectPath value. If "/x/repo" appears as the projectPath of the row keyed /x/repo/dbt, a session started at /x/repo passes the gate, though the cache has no key for it. That contradicts the newest commit's promise to run only where the link cache names the folder. The CLI checks the link again, so the cost is a CLI start that does nothing on every event, but the gate isn't doing what it says.

Suggested fix: match the key form only, for example grep -qE on "<escaped>"[[:space:]]*: (keys are always followed by :, even in pretty-printed JSON). Add a negative fixture where the folder appears only as a value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fb70ac1: the folder is matched only as a key, grep -qE '"<escaped>"[[:space:]]*:', with the JSON-escaped path also escaped for the regular expression. New fixtures: a folder that appears only as another row's projectPath value does not start the CLI, a dot in a folder name doesn't act as a wildcard, and a pretty-printed cache key still matches.

Comment on lines +1 to +3
#!/usr/bin/env bash
# Checks the workspace-rules hook against a stub CLI: hooks.json runs it for the right events, arguments and timeouts;
# it passes the hook input through, prints only the CLI's hook JSON at session start, starts the CLI for any event only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR (testing): nothing runs this test

#!/usr/bin/env bash
# Checks the workspace-rules hook against a stub CLI: hooks.json runs it for the right events, arguments and timeouts;

The repo has no workflow that runs the shell tests, and CONTRIBUTING only mentions tests/resolve-cli.sh. A later edit that breaks the link-cache walk would merge unnoticed, for example dropping the closing quote that stops /x/app from matching /x/app-old.

Suggested fix: list the new test in CONTRIBUTING's pre-PR steps. Better still, add a small workflow that runs bash tests/*.sh on macOS and Linux.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in fb70ac1: .github/workflows/tests.yml runs every tests/*.sh on ubuntu-latest and macos-latest (zsh installed on Linux for resolve-cli.sh), and CONTRIBUTING lists both tests and the current layout (the workspace-rules script, the three events, rules.md, tests/). The header nit is fixed too: it says the kill switch (ALTIMATE_LEARN / learn.enabled) gates team rules, and capture also needs learning from sessions (your choice, else the workspace setting).

- The link-cache check matches a folder only as a JSON key (quoted path then
  `:`), escaped for the regular expression, so a folder named only as another
  row's `projectPath` value does not start the CLI. New fixtures: a folder
  named only as a value, a dot in a folder name, and a pretty-printed cache.
- PRIVACY.md: team rules are given only while signed in as the account that
  pulled them; the rules state is in the user state directory from the
  altimate-code release that moves it there (earlier releases keep it in the
  project). Adds `rules_command`, says session events carry the session id, and
  names `ALTIMATE_LEARN_MODEL` ahead of `learn.model`.
- CI: a workflow runs `tests/*.sh` on Linux and macOS; CONTRIBUTING lists the
  tests and the current layout.
- The hook header says which switch gates what.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@hkaltai hkaltai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at fb70ac1 (independent Claude and Codex reviewers, one convergence round)

Approve, with conditions. All earlier findings are fixed or narrowed down to documentation. The hook can't fail the host, the link-cache gate is keyed on user-level state only, and the CLI contract matches altimate-code#1626: events, --host, the hookSpecificOutput JSON, proposals reply flags and failure codes. Both shell tests pass locally under macOS bash 3.2.

Before merge/release:

  • Resolve the merge conflict with main (the PR is CONFLICTING). That's why the new tests.yml hasn't reported a check here. Merge after it runs green on Linux and macOS.
  • Ship only after an altimate-code release that includes #1626 and #1669. #1626 currently has changes requested, for a blocker that #1669 fixes.
  • Fix the learning-off wording in PRIVACY.md (replied in the existing thread).

Inline (MINOR)

  • plugins/altimate-code/hooks-handlers/workspace-rules.sh:56-62: non-canonical keys in link caches written by older CLIs can keep the hooks from ever starting.
  • PRIVACY.md line 33: the learning-off guarantee is still too broad (reply in thread).
  • Release gate and stale PR description: needs #1669 too (reply in thread).

Nits

  • PRIVACY.md lines 3-4: the effective date stays at July while line 96 promises to "revise the effective date when our practices change". Either update it at release, or change line 96 to refer to "Last revised". (Contested between the reviewers; leave it to the legal sign-off.)
  • plugins/altimate-code/CHANGELOG.md: the [1.2.0] — 2026-10-30 date predates the release. Set it when tagging.
  • .github/workflows/tests.yml: actions/checkout@v4 is pinned by tag, not SHA. Low risk with contents: read.

Missing tests (advisory)

  • A link-cache fixture with a non-canonical (symlinked) key.
  • A live host run: the hook's working directory, the detached Stop process surviving the hook's exit, and SessionStart additionalContext from the second hook in the group.

Comment on lines +56 to +62
dir="$(pwd -P)"
[ -n "$top" ] && [ -n "$dir" ] || return 1
while :; do
key="$dir"
command -v cygpath >/dev/null 2>&1 && key="$(cygpath -w "$dir")"
key="$(printf '%s' "$key" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/[][\.*^$+?(){}|]/\\&/g')"
grep -qE "\"$key\"[[:space:]]*:" "$cache" 2>/dev/null && return 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR (logic error): link caches written by older CLIs can keep the hooks from ever starting

  dir="$(pwd -P)"
  [ -n "$top" ] && [ -n "$dir" ] || return 1
  while :; do
    key="$dir"
    command -v cygpath >/dev/null 2>&1 && key="$(cygpath -w "$dir")"
    key="$(printf '%s' "$key" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/[][\.*^$+?(){}|]/\\&/g')"
    grep -qE "\"$key\"[[:space:]]*:" "$cache" 2>/dev/null && return 0

The gate only matches the physical path. Caches written by older altimate-code builds can have keys that aren't real paths: a folder reached through a symlink, or /tmp/... instead of /private/tmp/... on macOS. The CLI rewrites those to canonical keys only during a CLI read that misses (readLocalBinding → migrateToCanonicalKeys, workspace/state.ts:337-352). This gate now stops that read from ever happening.

Failure scenario: a repo linked through a symlinked path with an older CLI. After upgrading, the person only uses this host. linked() never matches, so team rules aren't given and corrections aren't recorded, with nothing saying why. Running altimate learn status once in the folder "fixes" it as a side effect, which hides the cause.

Suggested fix: also try the logical pwd and its parents as keys. At minimum, add a README troubleshooting line ("after upgrading, run altimate learn status once in the linked folder"). Add a fixture with a symlinked key to pin down whichever behaviour you choose. Same script in the sibling plugin.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 390e882 (codex plugin: 5ef887e). linked tries the physical path first, then the path the host reached the folder by ($PWD: a symlink, or /tmp for /private/tmp). The walk on that path still stops at the folder whose physical path is the repository root. Tests: a link keyed by the symlinked path is found, and a link above the repository reached through the symlink is not. The first fails on the old script.

saravmajestic and others added 3 commits November 6, 2026 08:04
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps main's MCP mode, workspace skills, lineage and guard, and adds the workspace-rules hooks beside them (one Stop list, version 1.5.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and never matches an empty key

- `linked` tries the physical path, then the path as the host reached it (a symlink, `/tmp`), still bounded by the repository root.
- An empty key (a `cygpath` that prints nothing) no longer matches an empty string in the cache.
- PRIVACY.md: the change note names plugin 1.5.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
top="$(cd "$ROOT" 2>/dev/null && pwd -P)"
[ -n "$top" ] || return 1
linked_from "$cache" "$top" "$(pwd -P)" && return 0
[ -n "${PWD:-}" ] && [ "$PWD" != "$(pwd -P)" ] && linked_from "$cache" "$top" "$PWD"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The symlink walk can leave the repository and match an unrelated link

linked_from stops only when a folder's physical path equals $top (line 75) or when the walk reaches /. If the symlink sits outside the repository and points at a subfolder of it (~/projects/acme-link -> ~/work/acme-repo/dbt, session started in ~/projects/acme-link/models), no textual ancestor of $PWD has pwd -P equal to $top, so this second walk climbs acme-link -> ~/projects -> ~ -> /. A cache key for any of those (for example a workspace link on ~/projects) then makes linked succeed, and session start plus the per-turn hooks start the CLI in a project with no link -- contrary to lines 18-19 and PRIVACY.md's "Elsewhere they start nothing". The cost is a CLI start that does nothing (the CLI re-checks the physical path), not a data leak, but the gate is not doing what it says. The new test at line 163 only covers a symlink at the repository root, where pwd -P does reach $top, so it cannot catch this.

Bound each step to the repository and reuse the result:

phys="$(cd "$dir" 2>/dev/null && pwd -P)"
[ -n "$phys" ] || return 1
case "$phys" in "$top"|"$top"/*) ;; *) return 1 ;; esac

with [ "$phys" = "$top" ] && return 1 replacing line 75.

@ralphstodomingo ralphstodomingo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-read at 390e8823cd, against my 11-04 points, your 11-10 replies and AltimateAI/altimate-code#1626 as merged. I read it; I didn't run the tests, but CI ran them on Linux and macOS and they pass. Approving: everything I asked for is in, and the merge with main kept main's hooks.

My 11-04 points:

  • PRIVACY.md, all five:
    • the digest's contents: prompts, tool input and output up to 300 and 400 characters per call, and the final reply, 24,000 characters in all, with secrets masked and nothing else removed;
    • altimate-code's own model, named as where it goes;
    • a row for the rules telemetry;
    • the workspace's "Learn from sessions" setting as a way capture turns on;
    • a revised date.
  • The smaller ones: the description links AltimateAI/altimate-code#1605, the Windows check converts each folder with cygpath -w and JSON-escapes it, and the edit here-doc in commands/rules.md is flush left.

Today's commit:

  • The walk tries the physical path first, then $PWD, so it finds a link that an older CLI keyed by the symlinked path. An empty key (from a cygpath that prints nothing) returns before the grep. The new test cases cover both.
  • Kilo's new point on line 60 is right, but narrow. When a symlink outside the repository points at a folder inside it, no folder on the $PWD walk has the repository root as its physical path. So the walk climbs to /, and a link on a folder above the symlink (say ~/projects) lets the CLI start. The CLI then does its own check from the physical path, finds no link, and does nothing, so the cost is a wasted start, not a wrong result. But it's the one case where "elsewhere they start nothing" doesn't hold, and the new test (a symlink at the repository root) doesn't cover it. Kilo's bound, which stops when a folder's physical path is outside the repository, fixes it. It's worth fixing or answering before this merges.

The merge with main: hooks.json keeps main's session-start script, the hourly skill refresh and the warehouse guard alongside the three team-rules hooks. In every other file both sides touched (the 1.5.0 version, the changelog, README, CONTRIBUTING, PRIVACY and tests/resolve-cli.sh), the PR only adds to main.

Against AltimateAI/altimate-code#1626 as merged: learn hook takes these three events and --host claude, learn proposals reply has every flag /altimate-code:rules uses, and the link cache is altimate-workspace-bindings.json in the state directory, keyed by real path, which is how the hook reads it. The release gate in the description is still the one that matters: merge and release this after the first altimate-code release that includes AltimateAI/altimate-code#1626.

Appendix: complexity delta

No TypeScript or Python in this diff (a shell hook, its shell test, Markdown and JSON), so there's nothing to measure.

# Conflicts:
#	.claude-plugin/marketplace.json
#	plugins/altimate-code/.claude-plugin/plugin.json
#	plugins/altimate-code/CHANGELOG.md
@saravmajestic
saravmajestic merged commit 15ef117 into main Nov 10, 2026
4 checks passed
@saravmajestic
saravmajestic deleted the feat/workspace-rules branch November 10, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants