Repository navigation
feat: team rules from a linked Altimate workspace in Claude Code sessions - #13
saravmajestic wants to merge 4 commits into
Conversation
…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>
| PROPOSAL_ID='<the proposal id>' | ||
| WORDING="$(cat <<'ALTIMATE_RULE' | ||
| <the new wording> | ||
| ALTIMATE_RULE |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in a6a01db: the block is flush left and says to run it exactly as shown.
| | 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. |
There was a problem hiding this comment.
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.
| 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. |
|
|
||
| case "$EVENT" in | ||
| session-start) | ||
| OUTPUT="$(printf '%s' "$INPUT" | "$ALTIMATE_BIN" learn hook session-start --host "$HOST" 2>/dev/null)" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Already addressed (the link-cache gate); refined in a6a01db to the exact walk the CLI does.
Code Review SummaryStatus: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Files Reviewed (6 files in the incremental commit)
Behaviour against a real Previous Review Summaries (3 snapshots, latest commit 6e87e8f)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 6e87e8f)Status: 4 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (5 files, 4 in the incremental commit)
Behaviour against a real Previous review (commit 4899cf8)Status: 3 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (10 files, 3 in the incremental commit)
Incremental commit Fix these issues in Kilo Cloud Previous review (commit d95762e)Status: 3 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (10 files)
Notes: the hook script's resolver block matches the existing copies byte-for-byte and Reviewed by deepseek-v4.1-flash-1017 · Input: 53.1K · Output: 43.4K · Cached: 981.9K |
…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 |
There was a problem hiding this comment.
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 0There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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: "$ROOTonly locates the state file; the CLI runs in the host's directory (nocd), 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/appmatches/x/app-old). The walk-up fix in the inline comment covers both.- Docs mix
altimate learn …andaltimate-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
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
|
||
| ## 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
|
||
| - 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). |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in a6a01db: "Last revised: November 4, 2026" plus a dated note under Changes to this policy. Legal sign-off is still pending.
| 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; } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in a6a01db: hooks.json is parsed with python3, checking each event's argument and its 15/5/5 timeout.
ralphstodomingo
left a comment
There was a problem hiding this comment.
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:
- What the digest contains. The "session transcript" row says "a bounded, redacted digest". The digest altimate-code builds (
packages/opencode/src/altimate/learn/digest.tsin 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. - 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. - 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_draftedandrule_proposal_shown(the last with the host session id). Add a row to the table, or point this section at "Usage telemetry" below. - 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.
- 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 #1605links 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.mdis 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 |
There was a problem hiding this comment.
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.
|
Review round addressed in a6a01db. Each inline thread has a reply. PRIVACY.md, for ralphstodomingo's points 1–5 and hkaltai's comments:
Legal sign-off is still pending. Smaller points:
hkaltai's nits:
Dependency: merge and release after altimate-code#1626 ships in a release; the no-model and gating fixes are there. 🤖 Generated with Claude Code |
Summary
Team rules from a linked Altimate workspace now apply in Claude Code sessions. This needs altimate-code with
learn hook(AltimateAI/altimate-code#1626, which builds on the merged AltimateAI/altimate-code#1605). Merge and release after #1626 ships in an altimate-code release. With an older altimate-code the hooks do nothing: the session-start output is dropped unless it is the hook's JSON.SessionStart,UserPromptSubmit,Stop) callaltimate-code learn hook <event>with the host's hook input on stdin..altimate-code/learn/workspace-rules/rules.jsonexists./altimate-code:ruleslists the stored proposals, the team rules and your proposals' status, asks with Claude Code's question tool, and answers throughlearn proposals reply.Test plan
bash tests/resolve-cli.sh(now coverscommands/rules.mdandlearncalls)bash tests/workspace-rules-hook.sh(new, stub CLI):🤖 Generated with Claude Code