You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[bug] Delivery and local-state bugs on main found while scoping #915 #993
These bugs on main (f287dcb) came up while scoping #915. They do not depend on #915 and can be fixed in parallel with it. Five of them touch code #915 builds on (last column), so fixing those first saves a rebase. The #915 work is a PR stacked on the head of the PR that fixes these. Bug 11 was withdrawn after a check of the WorkBuddy app (see its section).
Each was reproduced with the built CLI 0.22.0 in a sandboxed HOME, except 6 (OpenCode V2 source and docs) and 10 (CodeBuddy CLI). The credential leak in the local agent's .codebuddy/models.json, found in the same analysis, is handled inside #915.
1. A pull in a linked worktree deletes the main checkout's state when the repo uses --separate-git-dir or is a submodule
Problem
Each checkout of a repo has a delivery record in state.lastPullByWorkspace and a workspaces/<id>/ directory in the data home, which holds the managed-MCP record, search index and resource cache. A full pull drops the records and directories of checkouts that git worktree list no longer reports (#808, #812).
In a repo created with git init --separate-git-dir, and in a submodule, git worktree list --porcelain names the git directory as the main entry, not the main checkout. A pull in any linked worktree then treats the main checkout as removed. It deletes the main checkout's workspaces/<id>/ and its delivery record.
git worktree add is enough to trigger it, because teamai's post-checkout hook pulls in the new worktree.
With the record gone, the main checkout's next pull has nothing to compare against. It overwrites a rule or skill the member edited there, with no warning.
Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo.
The edit is gone, and no "Kept" line was printed. The same steps in a normal repo keep both workspace directories and both records, and keep the edit.
Expected: a pull in a linked worktree leaves the main checkout's directory and record alone. The main checkout's next pull then keeps the edited rule, as it does in a normal repo.
Cause
listWorktrees (src/utils/git.ts) returns the worktree lines of git worktree list --porcelain. In these layouts, the first entry is the git directory (.../sepgit/proj.git, or .../super/.git/modules/<path> for a submodule), so the main checkout is missing from the list. Two callers in src/pull.ts use that list as proof of liveness:
liveCheckoutRecords keeps only the records whose checkoutKey matches a listed root.
pruneWorkspaceDirs (src/utils/partition.ts) deletes every workspaces/<id>/ whose managedMcpWorkspaceId is not in the list.
Related: listWorktrees splits the porcelain output on \n. A checkout path that contains a newline is cut short, so that worktree also counts as removed. git worktree list --porcelain -z returns the full path in one NUL-terminated field.
Define liveness by checking each recorded checkout, not by membership in the porcelain list. A checkout is live when its root exists and git -C <root> rev-parse --git-common-dir (realpath) equals this repo's common dir. Always count the current checkout as live.
When reading porcelain at all, use git worktree list --porcelain -z and skip entries marked bare or prunable, and an entry equal to the common dir. Apply the same rule in liveCheckoutRecords and pruneWorkspaceDirs.
Add regression cases for --separate-git-dir, a submodule, and a worktree path with a newline.
2. Without a delivery record for the checkout, pull deletes or overwrites rules the member edited, while edited skills are kept
Problem
Pull protects a copy the member edited only when the checkout has a delivery record (#822, #865). That record lives in state.lastPullByWorkspace under a key that includes the inode of the checkout's .git. When the record is missing, rule copies have no protection:
A rule the member edited and that is still delivered is overwritten.
A rule the member edited and that is no longer delivered (for example after teamai roles set) is deleted.
Neither case prints anything without --verbose. Skills behave differently. An edited skill that is no longer delivered is kept by a content check against the team repo, whether or not a record exists.
A record goes missing in normal use:
The checkout is restored from a backup, or copied (cp -a, rsync, Time Machine, a move across volumes). .git gets a new inode, so the key no longer matches.
A pull in a linked worktree prunes the main checkout's record in a --separate-git-dir repo or a submodule (bug 1 above).
listWorktrees misses a live checkout, for example one whose path contains a newline, so liveCheckoutRecords drops its record.
Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo. The team has roles frontend and backend, rules team-rule, frontend/fe-rule and backend/be-rule, and skills frontend/fe-skill and backend/be-skill.
cd biz
teamai init <team-url> --provider git --agent claude --role frontend --scope project
# give .git a new inode, as a restore or copy does
mv .git .git.old && cp -a .git.old .git && rm -rf .git.old
echo"member edit">> .claude/skills/fe-skill/SKILL.md
echo"member edit">> .claude/rules/frontend/fe-rule.md
echo"member edit">> .claude/rules/team-rule.md
teamai roles set backend
teamai pull
✔ [project] Synced 1 skills (1 new, 0 updated)
✔ [project] Synced 2 rule(s)
⚠ [project] Kept skill "fe-skill" (claude): it has local changes or unpushed files not in the team repo (or could not be verified). ...
$ find .claude -type f | sort
.claude/rules/backend/be-rule.md
.claude/rules/team-rule.md
.claude/skills/be-skill/SKILL.md
.claude/skills/fe-skill/SKILL.md
.claude/skills/teamai/SKILL.md
$ cat .claude/rules/team-rule.md
# Team rule
With --verbose, the same run on a second fixture prints [debug] Removed stale rule frontend/fe-rule.md from claude. It also shows the record key changing from 6d08ff919a31-361166912 to 6d08ff919a31-361167393.
Expected:
frontend/fe-rule.md is kept with the "Kept ... you changed this copy" warning, as it is when a record exists.
team-rule.md keeps the member's line, with the "Kept ... you changed it" notice.
Cause
openCheckoutLedger (src/pull.ts) reads the record for checkoutKey(projectRoot). When there is none, ledger.previous is undefined, and src/resources/delivered-copies.ts treats every copy as unchanged:
judgeCopy returns write, so a still-delivered rule is overwritten.
removedCopyChanged returns false, so the stale sweep in RulesHandler.pullAllRules (src/resources/rules.ts) removes a rule that is no longer delivered. Claude is not in RULE_FORMATS, so it takes the branch that relies on removedCopyChanged.
The skill path does not depend on the record. Its removal checks the copy's content against the team repo and keeps it on any difference.
The #822 design chose "without a record, pull writes as it always has" for a first install. It did not account for a record that is lost after delivery.
Impact
The member's work in rule files is lost without a message: deleted in one case, overwritten in the other. Once #915 hides delivered files from git status, git also has no trace of them to recover.
Proposed fix
When the checkout has no record for a copy, take it as teamai's only on proof: its bytes equal what teamai renders for that resource at some revision in the team repo's history (the same proof bug 12 needs). An unedited copy from an older team version is then updated as today; an edited one is kept and named, as an edited copy with a record is named. This also covers rules a team once committed to the repo.
A first install still writes wherever no file exists. The record is never carried over to a new key: #807 puts the inode in the key so that a re-created worktree starts clean.
Not checked
Agents. They use the same ledger, so they probably behave like rules.
Tools other than Claude. Cursor takes the same branch as Claude, so it should lose rules the same way. Tools that share their rules directory with the member's own rules (sharesRulesDirWithMember: Copilot, CodeBuddy, Kiro and others) remove a copy only when recordedUnchanged proves it is unchanged. They should keep the copy when there is no record. Neither was run.
Windows, where checkoutKey uses a different inode and birth-time behavior.
3. Codex skill delivery overwrites a member's own skill of the same name in .agents/skills/
Problem
For Codex, teamai delivers a skill into .agents/skills/<name>/ instead of .codex/skills/<name>/ whenever .agents/skills/<name>/ already exists (#435). Other tools share .agents/skills/, so it may hold a skill the member wrote, or one another tool installed. teamai treats it as its own and replaces the member's files with the team version. At init this happens silently. On a later pull it happens right after a warning that says both copies are kept.
Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo with the role skill frontend/fe-skill.
Case 1: the member's skill exists before they join the team.
cd biz
mkdir -p .agents/skills/fe-skill
printf -- '---\nname: fe-skill\ndescription: my own skill\n---\nMY OWN CONTENT\n'> .agents/skills/fe-skill/SKILL.md
teamai init <team-url> --provider git --agent codex --role frontend --scope project
The member's file was replaced, with no warning, and the skill was reported as "new".
Case 2: the member creates the skill after teamai delivered it to .codex/skills/.
teamai init <team-url> --provider git --agent codex --role frontend --scope project
mkdir -p .agents/skills/fe-skill
printf -- '---\nname: fe-skill\ndescription: my own skill\n---\nMY OWN CONTENT\n'> .agents/skills/fe-skill/SKILL.md
teamai pull --force
⚠ Codex skill conflict for fe-skill: keeping different copies in .agents/skills and .codex/skills
✔ [project] Synced 1 skills (all updated)
$ tail -1 .agents/skills/fe-skill/SKILL.md
# fe-skill
The warning says both copies are kept, but MY OWN CONTENT is gone.
Expected: teamai never wrote .agents/skills/fe-skill/, so the copy is the member's, and teamai leaves it alone. It delivers to .codex/skills/fe-skill/ and names the conflict.
Cause
resolveSkillDestination (src/resources/skills.ts) returns .agents/skills/<name> (SHARED_AGENT_SKILLS_PATH) for Codex whenever that path exists. When the two copies differ, it logs the conflict warning and still returns the shared path. SkillsHandler.pullItem then writes the team skill there.
The edit protection from #822 does not apply. judgeCopy (src/resources/delivered-copies.ts) finds no record for files teamai never wrote, and so treats the copy as safe to write.
Impact
Any directory in .agents/skills/ whose name matches a team skill is overwritten at init or on the next full pull. This includes a skill the member wrote, one installed by another tool, or one installed by hand. The loss is silent at init. On a later pull the only message is a warning that says the opposite.
Proposed fix
In resolveSkillDestination, return .agents/skills/<name> only when teamai owns that copy:
the checkout's delivery record lists files under it, or
its content equals the team skill at some revision in the team repo's history (the bug 12 proof), which includes the case the existing duplicate cleanup already handles.
teamai remove and teamai uninstall then delete only teamai's copy (.codex/skills/<name>) and leave a diverted member skill in .agents/skills/<name> alone.
Otherwise deliver to .codex/skills/<name>. Warn that Codex sees two skills with the same name and say which one is the member's. Change the warning text to match what happens.
Not checked
User scope (~/.agents/skills). The same function serves it, so it probably behaves the same, but it was not run.
Which skill Codex loads when both directories hold a skill with the same name.
Windows.
Environment
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git, agent Codex (directory only; the Codex binary was not run).
4. pull writes .teamai/managed-hooks.json into the project tree, where git add -A commits it
Problem
Since the data home moved to ~/.teamai/projects/<slug>/ (#374), a project's working tree should hold no teamai machine state. One file still lands in it: .teamai/managed-hooks.json, the index of team hooks teamai injected. Nothing ignores it.
Git project scope with Copilot enabled: a pull writes .teamai/managed-hooks.json (the Copilot entry). It is the only file under .teamai/, and it shows as ??.
Self mode (init .): every pull with team hooks writes it. .teamai/.gitignore does not list it, so git add -A commits one member's hook index to the team's main branch.
teamai packages install has the same cause. It writes .teamai/teamai.lock into the project, then creates .teamai/.gitignore to hide it, and that file shows as ?? in a non-self project.
Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare repos as remotes.
Git project scope. The team repo has hooks/hooks.yaml with one Stop hook and sharing.hooks.autoApply: true. ~/.copilot exists.
cd biz
teamai init <team-url> --provider git --agent claude,copilot --role frontend --scope project
git status --porcelain -uall | grep -v 'skills/\|rules/'
.teamai/managed-hooks.json holds {"copilot": [...]}. The data home next to it already has managed-main-checkout-hooks.json, so the Claude and Codex index has moved and the Copilot index has not.
Self mode: teamai init . --provider git --agent claude, then commit .teamai/hooks/hooks.yaml with one hook and run teamai pull.
Packages: the team teamai.yaml declares packages.npm: [{name: left-pad, version: "1.3.0"}]. The project has a package.json, and a stub npm that exits 0 is on PATH.
Expected: none of these files in the working tree. Each belongs in the data home, as managed-mcp.json and managed-main-checkout-hooks.json already do.
Cause
getManagedHooksPath (src/types.ts) builds the path from getTeamaiHome(scope, projectRoot), which is <projectRoot>/.teamai in project scope, not from getDataHome. Its callers include reconcileTeamHooksForConfig (src/hooks.ts, the Copilot manifest) and resolveHookScope (src/types.ts, self mode), plus hooks-cmd.ts and uninstall.ts.
pkgInstall and the package status check (src/pkg/commands.ts) also use getTeamaiHome for teamai.lock. ensurePackageLockIgnored (src/pkg/manifest.ts) then writes .teamai/.gitignore.
buildSelfModeGitignore (src/init.ts) lists teamai.lock and managed-mcp.json, but not managed-hooks.json.
Impact
In self mode, git add -A or a commit-all from an IDE publishes one member's hook index to the team. It contains the rendered hook commands for that member's role and projects.
In a git project, git status is never clean once Copilot or packages are in use.
Resolve getManagedHooksPath to the per-checkout workspace directory of the data home (workspaces/<id>/, where managed-mcp.json already lives), because the Copilot hook file and self-mode hooks are per checkout. Legacy readers (the pre-fix(hooks): unify hook-injection scope so project-scope init installs the SessionStart hook #370 Codex import, the legacy hook scope, uninstall) keep reading <root>/.teamai/managed-hooks.json through a separate helper. On the first pull after upgrade, move only the copilot key (project scope) or every key (self mode) out of the legacy file; delete the legacy file only when it is then empty and untracked, and have doctor name a tracked one.
Write teamai.lock to the data home and stop creating .teamai/.gitignore for it in a non-self project. Move an existing .teamai/teamai.lock, and remove a .teamai/.gitignore that holds only teamai's line.
Add managed-hooks.json to buildSelfModeGitignore, and to migrateSelfModeGitignore for existing installs.
Not checked
Windows.
A real npm install: a stub npm was used, so the lock content reflects the stub.
Whether any released version committed this file in a self-mode repo. A fix that relocates the file should also git rm --cached it, or doctor should name it, when it is tracked.
5. In a repo without .git/info/, pull says ".git/info is not writable" and withholds project MCP servers
Problem
#886 keeps a project MCP config with resolved ${VAR} values out of git by adding it to .git/info/exclude before writing it. Some repos have no .git/info/ directory: those created with git init --template=, with an empty init.templateDir, or by some tools. In such a repo, teamai reports that .git/info "is not writable" and withholds every team MCP server whose config holds a resolved value. The directory is not read-only. It does not exist, and teamai could create it.
Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo:
mcp/mcp.yaml has one server, secret-api, with the header Authorization: Bearer ${LAB_TOKEN}.
env/env.yaml sets LAB_TOKEN.
sharing.mcp.autoApply: true.
mkdir biz &&cd biz &&echo'# app'> README.md
git init -q -b main --template= && git add -A && git commit -qm app
ls .git # no info/
teamai init <team-url> --provider git --agent claude --role frontend --scope project
⚠ Did not write claude's MCP servers to .../biz/.mcp.json: it would hold resolved values, and teamai could not keep it out of git first: .../biz/.git/info is not writable. The file is left as it was. Make it writable, or add `/.mcp.json` to it yourself, then run `teamai pull` again.
⚠ ✖ MCP servers delivered to claude
→ In .../biz/.mcp.json, withheld: secret-api, as git would commit the file: .../biz/.git/info is not writable. ...
Afterwards, .mcp.json and .git/info are both absent. After mkdir .git/info && teamai pull --force, the server is written and .git/info/exclude holds the teamai block.
Expected: teamai creates .git/info/, adds /.mcp.json to exclude, and writes the server, with no warning.
Cause
ensureExcludedFromGit (src/mcp-git-exclude.ts) checks fse.access(path.dirname(excludeFile), W_OK) before it writes. access on a missing directory fails with ENOENT, which the function reports as "not writable". The write that follows, writeFileAtomic, would have created the directory with ensureDir.
The fix hint, "add /.mcp.json to it yourself", also points at the directory rather than the exclude file.
Impact
In these repos, members never get MCP servers whose config uses team env or secrets, and every pull and doctor run shows the warning.
The message sends the member to fix permissions that are not wrong.
In ensureExcludedFromGit, check writability on the nearest existing ancestor of the exclude file, and let the write create info/. Report "not writable" only for EACCES, EPERM or EROFS. Name the exclude file, not its directory, in the hint.
Add a test that removes .git/info and expects the server to be written and excluded.
Not checked
Linked worktrees and submodules without info/. The exclude path comes from git rev-parse --git-path info/exclude, so the fix should be the same.
Which clone tools or hosting setups produce repos without info/ in practice.
6. OpenCode V2 gets no team rules or team instructions: V2 ignores the instructions entries teamai relies on
Problem
teamai delivers team rules and its instruction file to OpenCode by listing them in an OpenCode config's instructions array:
Project scope: .opencode/opencode.json gets the .opencode/rules/**/*.md glob and .opencode/teamai-context.md.
User scope: ~/.config/opencode/opencode.json gets the user rules globs and the absolute path of the instruction file.
OpenCode V2 parses instructions and then ignores it. Members on OpenCode V2 get none of the team rules, and none of the culture, claudemd/ or recall blocks. teamai doctor still reports them as active.
#982 added V2 support to teamai's OpenCode plugin, but only for hooks.
packages/schema/src/config.ts declares instructions as an optional array of strings.
packages/core/src/config/plugin/instruction.ts is the only instruction source. It loads the global AGENTS.md and the AGENTS.md in each ancestor directory, and nothing else. No code in packages/ reads instructions from a config document. grep -rn -E '(info|config|document|doc|entry)\??\.instructions' packages returns nothing.
The V2 docs at that tag (instructions.mdx, https://opencode.ai/v2/docs/instructions/) say: "The V2 config schema accepts an instructions array, but V2 does not currently resolve its files, glob patterns, or URLs. This configuration does not add instructions to the model yet." They also say V2 "recognizes AGENTS.md only. It does not use CLAUDE.md as a fallback."
teamai side, built CLI 0.22.0 at f287dcb, sandboxed HOME with ~/.config/opencode:
{
"instructions": [
".opencode/rules/**/*.md"
]
}
✔ opencode is installed
✔ Skills delivered to opencode
✔ Team rules are active in opencode
✔ Rules delivered to opencode
✔ Team instructions are current for opencode
The installed plugin, ~/.config/opencode/plugin/teamai-hooks.ts, is built by buildDualPluginDefinition (src/opencode-hooks.ts). Its V2 setup(ctx) registers prompt and tool.execute.after hooks and an event subscription. It adds no context to the system prompt.
Cause
On V2, the only delivery channel teamai uses for rules and instructions is the instructions entry: opencodeProjectRuleGlobs, the user rules globs and opencodeContextReference (src/resources/opencode-config.ts). In user scope, opencodeClaudeFallback relies on OpenCode loading ~/.claude/CLAUDE.md, which V2 does not do either.
Impact
OpenCode V2 members silently work without the team's rules and instructions, in both scopes.
doctor reports success, so nothing points at the gap.
Deliver through the plugin teamai already installs in HOME. In its V2 setup(ctx), register ctx.session.hook('context', ...) and the same text on compaction. The plugin reads the files itself: from ctx.location.directory it walks up to the nearest directory holding .opencode/teamai-context.md or .opencode/rules/, and pushes teamai-context.md followed by every .opencode/rules/**/*.md (the rules teamai delivered for the member's selection), sorted by path. In every directory it also pushes the user-scope rules and instruction file, as V1 does through the user config. When the user target is the CLAUDE.md fallback, teamai also writes ~/.config/opencode/teamai-context.md for V2.
Keep the instructions entries for V1, which does resolve them. Have doctor check the installed OpenCode major version and report V2 delivery from the plugin, not from instructions.
Not checked
An end-to-end V2 session. Confirming that the context hook reaches the model needs a model request, and none was made. Both the failure and the proposed hook rest on V2 source and docs.
Whether a later V2 release resolves instructions. The docs call it "not currently" supported, and migrate-v1.mdx lists instructions as needing no migration, which contradicts them.
OpenCode V1 is unaffected according to its source (packages/opencode/src/session/instruction.ts at v1.18.35). It was not run.
Environment
macOS 27.0, Node.js 24.21.0, teamai 0.22.0 (f287dcb), OpenCode v2.0.24 (source and docs; the installed binary reports opencode v2.0.24).
7. In project scope, the co-author setting is written into the team's tracked .claude/settings.json
Problem
When the team sets sharing.coAuthor.enabled: false, or a member sets coAuthorEnabled, teamai writes attribution into the project's .claude/settings.json. It also writes to .codebuddy/settings.json, which the #915 delivery inventory observed, and to the settings of other Claude-family tools. Many teams track .claude/settings.json, so the first pull leaves it modified in every member's checkout. teamai also rewrites the whole file, which reformats it.
Since #955 (PR #958), team hooks in project scope go to .claude/settings.local.json to keep the shared file clean. The co-author reconcile still writes to the shared file.
Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo whose teamai.yaml has sharing.coAuthor.enabled: false. The business repo tracks .claude/settings.json:
Expected: .claude/settings.json is unchanged. The setting goes to a file that is local to the member.
Cause
resolveTargets (src/coauthor-reconcile.ts) targets path.join(baseDir, paths.settings) for the Claude family, and baseDir is the project root in project scope. applyClaude reads, patches and rewrites that file with writeJson. Codex and Cursor are already skipped in project scope ("co-author is user-scope only"). The Claude family is not.
Impact
Every member's checkout shows a modified tracked file after the first pull.
If one member commits it, their co-author choice becomes the project's. coAuthorEnabled is a per-member override, so a member with a different setting rewrites the tracked file on their next pull.
The rewrite also reformats the file, so the diff is larger than the change.
In project scope, write the Claude family's attribution to settings.local.json, the same personal layer #955 uses for team hooks. Do the same for CodeBuddy if it has a local settings layer. Otherwise skip it in project scope, as Codex and Cursor are skipped.
On the next pull, remove an attribution that teamai wrote to the shared file before the fix, but only when it is teamai's {"commit": "", "pr": ""} and coAuthorManaged records the file.
Not checked
That Claude Code reads attribution from settings.local.json. Its settings documentation says local project settings override shared ones, but this was not run.
8. Source skills go to the project's .hermes/skills and .openclaw/skills, which those tools never read, and to tools the member did not enable
Problem
Skills from a team's sources: take a different delivery path from the team's own skills, and they land in the wrong places:
Hermes and OpenClaw get the team's skills in their homes (~/.hermes/skills, <openclaw workspace>/skills). Source skills go instead to <project>/.hermes/skills/ and <project>/.openclaw/skills/, which neither tool reads. Hermes and OpenClaw members never receive source skills, and the project gets two dead copies.
Any tool directory that exists in the project receives source skills, even when that tool is not in enabledAgents.
Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed HOME with ~/.hermes and ~/.openclaw/workspace, local bare repos.
The team teamai.yaml has sources: [{name: other, repo: <other-url>}]. The source repo publishes other-skill, and the team has the role skill frontend/fe-skill.
cd biz
teamai init <team-url> --provider git --agent claude,hermes,openclaw --role frontend --scope project
git status --porcelain -uall | grep skills
find ~/.hermes ~/.openclaw -name SKILL.md
Resolve source skill targets through the same seam as team skills: skillsDirForTool for the directory, then resolveSkillDestination, after an isAgentExcluded check.
The source manifest records project-relative paths and rejects any path outside the project root, and the Hermes home and OpenClaw workspace are shared by every project subscribed to the same source. Record those destinations as absolute paths in the source manifest, owned per (source, project), and remove a copy there only when no other project's manifest for the same source still lists it, as team skills in those homes already work.
On the next pull, reclaim the copies an earlier pull left in <project>/.hermes/skills/ and <project>/.openclaw/skills/, using the source's install records. Reclaim a copy only when it equals the source skill at some revision of the source repo's history (the bug 12 proof); keep and name a changed one.
Add a test that delivers one source skill and one team skill to every built-in tool and asserts that both land in the same directory.
Not checked
User scope. There baseDir is HOME, so Hermes is probably right by accident and OpenClaw probably wrong (~/.openclaw/skills instead of the workspace). Not run.
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider git. Hermes and OpenClaw were not run; only their directories exist.
9. teamai init . --agent claude still sets up every tool found in HOME
Problem
In single-repo mode, --agent is meant to choose which tools teamai sets up in the repo. On main it has no effect. teamai init . --agent claude still:
injects hooks into the project for every tool that has a directory in HOME (Codex, Cursor, CodeBuddy, Copilot and so on),
saves all of them in enabledAgents.
Every later pull then delivers skills and hooks to those tools as well. The extra hook files are never committed, so they stay as untracked files in the team's repo.
Reproduction
Built CLI 0.22.0 at f287dcb. Sandboxed HOME with ~/.claude, ~/.codex, ~/.cursor, ~/.codebuddy and ~/.copilot. Repo self, whose origin is a local bare repo. init . requires an https origin, so the lab exposed it as one.
cd self
teamai init . --provider git --agent claude --verbose
git status --porcelain -uall
grep -A6 enabledAgents ~/.teamai/projects/*/config.yaml
✔ Created .teamai/teamai.yaml (mode: self)
✔ Updated teamai hooks in .../self/.claude/settings.json
✔ Updated teamai hooks in .../self/.codex/hooks.json
✔ Updated teamai hooks in .../self/.cursor/hooks.json
✔ Updated teamai hooks in .../self/.codebuddy/settings.json
✔ Updated teamai hooks in .../self/.github/hooks/teamai.json
[debug] git hook: installed teamai hooks in .../self
[debug] [bootstrap] Codex trust: trusted
✔ Local config saved to .../config.yaml
[debug] Seeded tool dirs for: claude, codex, cursor, copilot, codebuddy
[debug] teamai hooks already up-to-date in .../self/.claude/settings.json
✔ Committed .teamai/ skeleton to the current branch
?? .codebuddy/settings.json
?? .codex/hooks.json
?? .cursor/hooks.json
?? .github/hooks/teamai.json
enabledAgents:
- claude
- codex
- cursor
- copilot
- codebuddy
The [bootstrap] line, and the hook writes before "Local config saved", come from the clone-time self-heal, not from init. After a team hook is added and a pull runs, .codex/skills/, .cursor/skills/, .codebuddy/skills/ and .github/skills/ also receive the teamai skill.
Expected:
enabledAgents: [claude].
Hooks only in .claude/settings.json, which init commits.
No files for the other tools.
Cause
initSelfRepo (src/init.ts) writes .teamai/teamai.yaml with mode: self. Only later does it call loadLocalConfigForScope('project', businessRepoRoot) to read the existing config.
With mode: self on disk and no config yet, that load runs the clone-time self-heal: readConfigFrom → bootstrapSelfRepo (src/config.ts, src/bootstrap.ts). The self-heal sets enabledAgents to detectHomeInstalledAgents(), injects hooks for all of them and saves the config. initSelfRepo then takes the union of that "existing" config and --agent, so the filter never applies.
PR #852 diagnosed the same cause ("Bug 2") and fixed it by reading the config once, before teamai.yaml is written. It was closed without merging. The dry-run half was later handled by refusing init --dry-run (#900). The --agent half is still open.
Impact
--agent does nothing in single-repo mode whenever other tools are installed in HOME.
The repo gets untracked hook files for tools the team never chose. A later git add -A commits them.
Every member's pulls deliver to tools they did not select.
init also runs a full self-heal as a side effect, including registering git hooks and Codex trust, before its own steps.
Proposed fix
Read the pre-init project config once, before teamai.yaml is written, and use that snapshot for every later step: inheritUserScope, the enabledAgents union, the tool-roots carry-over and settleModeSwitch. This is #852's approach.
Alternatively, give loadLocalConfigForScope a no-self-heal option for init. Either way, add a test that runs init . --agent claude with other tools in HOME and asserts enabledAgents: [claude] and no other tool's files.
Not checked
The interactive picker path (no --agent, with a TTY). It probably merges in the same way.
Re-running init . on a repo that already has a config. There the load returns the real config and the self-heal does not run.
10. CodeBuddy reads only the first user MCP file that exists, so teamai's servers and the member's own servers hide each other
Problem
In user scope, CodeBuddy reads exactly one MCP file: the first that exists of ~/.codebuddy/.mcp.json, ~/.codebuddy/mcp.json and ~/.codebuddy.json. It does not merge them. teamai always writes the second one, ~/.codebuddy/mcp.json. So:
A member who added a server with codebuddy mcp add -s user has ~/.codebuddy/.mcp.json. teamai's team servers are then never loaded, and nothing says so.
A member whose servers live in the legacy ~/.codebuddy.json loses them as soon as teamai creates ~/.codebuddy/mcp.json.
Reproduction
CodeBuddy Code 2.161.4, sandboxed HOME.
With teamai's ~/.codebuddy/mcp.json present, the member's usr-srv in ~/.codebuddy.json disappears from codebuddy mcp list.
With ~/.codebuddy/.mcp.json present, teamai's tm-user disappears.
In a fresh home, codebuddy mcp add -s user my-user ... creates ~/.codebuddy/.mcp.json; after that, teamai's tm-user is no longer listed.
Servers in the local scope (~/.codebuddy.json → projects[...]) are unaffected.
The CodeBuddy docs state the rule: "The system does not merge multiple configuration files within the same scope; it only uses the first existing file." (https://www.workbuddy.ai/docs/cli/mcp)
Cause
toolPaths.codebuddy.mcp (src/types.ts) is fixed to .codebuddy/mcp.json, the second candidate in CodeBuddy's lookup order.
Impact
Team MCP servers silently fail to load for members who use CodeBuddy's own mcp add, or teamai silently hides the member's existing servers. doctor reports the team servers as delivered in both cases.
Proposed fix
In user scope, write to the first CodeBuddy user MCP file that exists, and only create ~/.codebuddy/.mcp.json (CodeBuddy's recommended file) when none exists. Have doctor warn when the file teamai writes is not the one CodeBuddy reads. WorkBuddy is not affected: its app reads ~/.workbuddy/mcp.json, the file teamai writes (confirmed in the app).
Not checked
Whether an existing ~/.workbuddy/.mcp.json shadows ~/.workbuddy/mcp.json in the WorkBuddy app.
Migrating servers teamai already wrote to ~/.codebuddy/mcp.json when the member also has ~/.codebuddy/.mcp.json.
11. Withdrawn: WorkBuddy's project MCP servers go to .workbuddy/mcp.json, which the WorkBuddy engine does not read (needs a check in the app)
Withdrawn 2026-10-07: not a bug. The WorkBuddy app reads <project>/.workbuddy/mcp.json and ~/.workbuddy/mcp.json itself (Sidebar → Plugins (插件) → MCP Server (MCP 服务器), or Connectors → Custom Connector → Configure MCP (配置 MCP)), so teamai's target is correct; the engine run alone does not read it, which is what this report observed.
Problem
In project scope teamai writes WorkBuddy's team MCP servers to <project>/.workbuddy/mcp.json (toolPaths.workbuddy.mcpProject in src/types.ts; docs/usage-guide.md). The engine WorkBuddy runs (the CodeBuddy engine) reads project servers only from <project>/.mcp.json or <project>/mcp.json. If the desktop app does not read .workbuddy/mcp.json itself, WorkBuddy members get no team project servers from it.
Evidence
Engine, verified on the CodeBuddy Code 2.161.4 CLI run the way WorkBuddy runs it (CODEBUDDY_CONFIG_DIR=WORKBUDDY_CONFIG_DIR=$HOME/.workbuddy), with .mcp.json (proj-srv) and .workbuddy/mcp.json (in-wb-mcp) in the project: codebuddy mcp list shows proj-srv, the user servers in ~/.workbuddy/mcp.json and the local ones in ~/.workbuddy/.codebuddy.json, but not in-wb-mcp.
The engine accepts servers passed in by the app (marked _workbuddyMcpOrigin: "custom"), so the app could read .workbuddy/mcp.json itself. That was not checked.
Check needed (one session with the WorkBuddy app)
Put a server only in <project>/.workbuddy/mcp.json, open the project, and see whether it is listed.
Put one in ~/.workbuddy/.codebuddy.json under projects["<opened folder realpath>"].mcpServers and see whether it is listed.
If check 1 fails, this bug is confirmed. If check 2 passes, the fix is to deliver WorkBuddy's project servers to that local scope, keyed by the realpath of each worktree root (the key CodeBuddy uses), which also keeps them out of the repo.
Impact (if confirmed)
WorkBuddy-only members get no team project MCP servers; members who also have Claude or CodeBuddy get them only through .mcp.json. doctor reports them as delivered.
When a project tool folder already holds a file or directory at the path where teamai delivers a team resource, or an MCP config already holds a server with the same name as a team server, and the checkout has no delivery record for it, pull and init replace it with the team version without a word. The output reports the resource as synced.
It hits a member who wrote a skill, rule or agent before joining the team, a resource installed by another tool or by hand, a member's own MCP server that happens to share a team server's name, and any copy whose record was lost (bug 1, bug 2). Bug 3 is the Codex .agents/skills case of the same failure.
Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed HOME, local bare team repo with a skill team-skill, a rule team-rule, an agent team-agent, a doc guide.md, a claudemd/ fragment, an MCP server plain-api and a Stop team hook. Before init, the business repo holds the member's own files at the same paths and names:
cd biz
# the member's own files, written by hand
.claude/skills/team-skill/SKILL.md "MY SKILL"
.claude/rules/team-rule.md "MY RULE"
.claude/agents/team-agent.md "MY AGENT"
.teamai/docs/guide.md "MY GUIDE"
.claude/rules/teamai-context.md "MY CONTEXT"
.mcp.json plain-api -> https://mine.example.com/mcp
.claude/settings.local.json Stop hook "echo my-stop"
teamai init <team-url> --provider git --agent claude --scope project
teamai pull --force
Member's file
After init and pull
Message
skill
team version
none ("Synced 1 skills (all updated)")
rule
team version
none
agent
team version
none
doc
team version
none
MCP server plain-api
team URL
"MCP: 1 change(s) across 1 server(s)"
teamai-context.md
kept
"was not written by teamai, so teamai left it unchanged"
Stop hook
kept; teamai adds its own marked entry
none needed
A second run shows the other side of the same cause. With teamai's main-checkout hook manifest removed (as when the record is lost), teamai pull --force adds a second, identical [teamai:hook:team-stop] entry to .claude/settings.local.json next to the one it wrote before, so the team hook runs twice. The usage guide states that "unrecorded or ambiguous legacy team-hook copies are preserved", and a new copy is then written.
Expected: every resource behaves like teamai-context.md: the member's file or server is left alone and named, and the team version is not delivered there until the member resolves it.
Cause
Ownership without a record defaults to teamai:
Skills and agents: SkillsHandler.pullItem and the agents handler skip a destination only when keepsEditedCopy says so; classifyCopy (src/resources/delivered-copies.ts) keeps a copy only when a file has a recorded hash. A destination with no record is always written.
Rules: the same ledger (judgeCopy); bug 2 is the lost-record case of it.
Docs: DocsHandler.pullDocs writes the mirror without checking an existing file it never wrote.
MCP: the reconcile writes team servers by name; a same-name server not in teamai's MCP manifest is replaced.
Hooks: the hook reconcile owns only entries its manifest records; an unrecorded copy teamai wrote earlier is preserved, and a new one is added.
The safe cases already prove ownership in the file: teamai-context targets check teamai's markers (planFile), and hooks write marked entries next to the member's.
Impact
A member's skill, rule, agent, doc or MCP server is lost the first time the team publishes one with the same name, or on the first pull after the record is lost. Only the MCP case prints anything, and it reads as a normal change.
Proposed fix
One ownership rule for every resource without a record, the one bug 2 needs too:
A destination that does not exist is written, as today (first install).
An existing file or directory with no record is teamai's only on proof: its content equals that resource at some revision in the team repo's history (for a source skill, the source repo's), compared by git blob ids. An older team copy is then updated as today.
Otherwise it is foreign: not written, named in the pull output with the path and how to resolve it (rename or delete it, then pull), and the team version is not delivered there. doctor lists it.
Entries in shared config files (MCP servers, hook entries in tool settings and hook files): an entry with no record that exactly equals what teamai renders for a team server or hook, at the current revision or any revision in the team repo's history, and matches only one, is adopted into teamai's manifest and then updated or removed as teamai's. Any other unrecorded entry is the member's: kept and named. For MCP, a same-name member server means the team server is not written to that file.
An existing record keeps today's behavior.
Not checked
Tools other than Claude; user scope.
The cost of the history comparison on large team repos.
.github/copilot-instructions.md and the CLAUDE.md blocks of the internal Claude variants (they use marked blocks, like teamai-context).
Hook duplication was run for Claude's settings.local.json; Codex hooks.json and other hook files follow the same manifest logic and were not run.
These bugs on
main(f287dcb) came up while scoping #915. They do not depend on #915 and can be fixed in parallel with it. Five of them touch code #915 builds on (last column), so fixing those first saves a rebase. The #915 work is a PR stacked on the head of the PR that fixes these. Bug 11 was withdrawn after a check of the WorkBuddy app (see its section).Each was reproduced with the built CLI 0.22.0 in a sandboxed
HOME, except 6 (OpenCode V2 source and docs) and 10 (CodeBuddy CLI). The credential leak in the local agent's.codebuddy/models.json, found in the same analysis, is handled inside #915.--separate-git-diror is a submodule.agents/skills/.teamai/managed-hooks.jsoninto the project tree, wheregit add -Acommits it.git/info/, pull says ".git/info is not writable" and withholds project MCP serversinstructionsentries teamai relies on.claude/settings.json.hermes/skillsand.openclaw/skills, which those tools never read, and to tools the member did not enableteamai init . --agent claudestill sets up every tool found in HOME--agentignoredWorkBuddy's project MCP servers go to.workbuddy/mcp.json, which the WorkBuddy engine does not read (needs a check in the app)1. A pull in a linked worktree deletes the main checkout's state when the repo uses
--separate-git-diror is a submoduleProblem
Each checkout of a repo has a delivery record in
state.lastPullByWorkspaceand aworkspaces/<id>/directory in the data home, which holds the managed-MCP record, search index and resource cache. A full pull drops the records and directories of checkouts thatgit worktree listno longer reports (#808, #812).In a repo created with
git init --separate-git-dir, and in a submodule,git worktree list --porcelainnames the git directory as the main entry, not the main checkout. A pull in any linked worktree then treats the main checkout as removed. It deletes the main checkout'sworkspaces/<id>/and its delivery record.git worktree addis enough to trigger it, because teamai'spost-checkouthook pulls in the new worktree.With the record gone, the main checkout's next pull has nothing to compare against. It overwrites a rule or skill the member edited there, with no warning.
Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed
HOME, local bare team repo.The workspace IDs are
sep=5caaebeed5d7andsep-wt=821f29cdc362.Then, in the main checkout:
The edit is gone, and no "Kept" line was printed. The same steps in a normal repo keep both workspace directories and both records, and keep the edit.
Expected: a pull in a linked worktree leaves the main checkout's directory and record alone. The main checkout's next pull then keeps the edited rule, as it does in a normal repo.
Cause
listWorktrees(src/utils/git.ts) returns theworktreelines ofgit worktree list --porcelain. In these layouts, the first entry is the git directory (.../sepgit/proj.git, or.../super/.git/modules/<path>for a submodule), so the main checkout is missing from the list. Two callers insrc/pull.tsuse that list as proof of liveness:liveCheckoutRecordskeeps only the records whosecheckoutKeymatches a listed root.pruneWorkspaceDirs(src/utils/partition.ts) deletes everyworkspaces/<id>/whosemanagedMcpWorkspaceIdis not in the list.Related:
listWorktreessplits the porcelain output on\n. A checkout path that contains a newline is cut short, so that worktree also counts as removed.git worktree list --porcelain -zreturns the full path in one NUL-terminated field.Impact
Proposed fix
Define liveness by checking each recorded checkout, not by membership in the porcelain list. A checkout is live when its root exists and
git -C <root> rev-parse --git-common-dir(realpath) equals this repo's common dir. Always count the current checkout as live.When reading porcelain at all, use
git worktree list --porcelain -zand skip entries markedbareorprunable, and an entry equal to the common dir. Apply the same rule inliveCheckoutRecordsandpruneWorkspaceDirs.Add regression cases for
--separate-git-dir, a submodule, and a worktree path with a newline.Not checked
.../.git/modules/<path>) is from the [feat] keep the resources pull delivers in project scope out of git #915 git-topology analysis.teamai pull.-zneeds git 2.36 or later. The minimum git version teamai supports was not checked.Environment
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider
git, agent Claude.2. Without a delivery record for the checkout, pull deletes or overwrites rules the member edited, while edited skills are kept
Problem
Pull protects a copy the member edited only when the checkout has a delivery record (#822, #865). That record lives in
state.lastPullByWorkspaceunder a key that includes the inode of the checkout's.git. When the record is missing, rule copies have no protection:teamai roles set) is deleted.Neither case prints anything without
--verbose. Skills behave differently. An edited skill that is no longer delivered is kept by a content check against the team repo, whether or not a record exists.A record goes missing in normal use:
cp -a, rsync, Time Machine, a move across volumes)..gitgets a new inode, so the key no longer matches.--separate-git-dirrepo or a submodule (bug 1 above).listWorktreesmisses a live checkout, for example one whose path contains a newline, soliveCheckoutRecordsdrops its record.Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed
HOME, local bare team repo. The team has rolesfrontendandbackend, rulesteam-rule,frontend/fe-ruleandbackend/be-rule, and skillsfrontend/fe-skillandbackend/be-skill.With
--verbose, the same run on a second fixture prints[debug] Removed stale rule frontend/fe-rule.md from claude. It also shows the record key changing from6d08ff919a31-361166912to6d08ff919a31-361167393.Expected:
frontend/fe-rule.mdis kept with the "Kept ... you changed this copy" warning, as it is when a record exists.team-rule.mdkeeps the member's line, with the "Kept ... you changed it" notice.Cause
openCheckoutLedger(src/pull.ts) reads the record forcheckoutKey(projectRoot). When there is none,ledger.previousisundefined, andsrc/resources/delivered-copies.tstreats every copy as unchanged:judgeCopyreturnswrite, so a still-delivered rule is overwritten.removedCopyChangedreturnsfalse, so the stale sweep inRulesHandler.pullAllRules(src/resources/rules.ts) removes a rule that is no longer delivered. Claude is not inRULE_FORMATS, so it takes the branch that relies onremovedCopyChanged.The skill path does not depend on the record. Its removal checks the copy's content against the team repo and keeps it on any difference.
The #822 design chose "without a record, pull writes as it always has" for a first install. It did not account for a record that is lost after delivery.
Impact
The member's work in rule files is lost without a message: deleted in one case, overwritten in the other. Once #915 hides delivered files from
git status, git also has no trace of them to recover.Proposed fix
When the checkout has no record for a copy, take it as teamai's only on proof: its bytes equal what teamai renders for that resource at some revision in the team repo's history (the same proof bug 12 needs). An unedited copy from an older team version is then updated as today; an edited one is kept and named, as an edited copy with a record is named. This also covers rules a team once committed to the repo.
A first install still writes wherever no file exists. The record is never carried over to a new key: #807 puts the inode in the key so that a re-created worktree starts clean.
Not checked
sharesRulesDirWithMember: Copilot, CodeBuddy, Kiro and others) remove a copy only whenrecordedUnchangedproves it is unchanged. They should keep the copy when there is no record. Neither was run.checkoutKeyuses a different inode and birth-time behavior.Environment
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider
git, agent Claude.3. Codex skill delivery overwrites a member's own skill of the same name in
.agents/skills/Problem
For Codex, teamai delivers a skill into
.agents/skills/<name>/instead of.codex/skills/<name>/whenever.agents/skills/<name>/already exists (#435). Other tools share.agents/skills/, so it may hold a skill the member wrote, or one another tool installed. teamai treats it as its own and replaces the member's files with the team version. Atinitthis happens silently. On a later pull it happens right after a warning that says both copies are kept.Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed
HOME, local bare team repo with the role skillfrontend/fe-skill.Case 1: the member's skill exists before they join the team.
The member's file was replaced, with no warning, and the skill was reported as "new".
Case 2: the member creates the skill after teamai delivered it to
.codex/skills/.The warning says both copies are kept, but
MY OWN CONTENTis gone.Expected: teamai never wrote
.agents/skills/fe-skill/, so the copy is the member's, and teamai leaves it alone. It delivers to.codex/skills/fe-skill/and names the conflict.Cause
resolveSkillDestination(src/resources/skills.ts) returns.agents/skills/<name>(SHARED_AGENT_SKILLS_PATH) for Codex whenever that path exists. When the two copies differ, it logs the conflict warning and still returns the shared path.SkillsHandler.pullItemthen writes the team skill there.The edit protection from #822 does not apply.
judgeCopy(src/resources/delivered-copies.ts) finds no record for files teamai never wrote, and so treats the copy as safe to write.Impact
Any directory in
.agents/skills/whose name matches a team skill is overwritten atinitor on the next full pull. This includes a skill the member wrote, one installed by another tool, or one installed by hand. The loss is silent atinit. On a later pull the only message is a warning that says the opposite.Proposed fix
In
resolveSkillDestination, return.agents/skills/<name>only when teamai owns that copy:teamai removeandteamai uninstallthen delete only teamai's copy (.codex/skills/<name>) and leave a diverted member skill in.agents/skills/<name>alone.Otherwise deliver to
.codex/skills/<name>. Warn that Codex sees two skills with the same name and say which one is the member's. Change the warning text to match what happens.Not checked
~/.agents/skills). The same function serves it, so it probably behaves the same, but it was not run.Environment
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider
git, agent Codex (directory only; the Codex binary was not run).4. pull writes
.teamai/managed-hooks.jsoninto the project tree, wheregit add -Acommits itProblem
Since the data home moved to
~/.teamai/projects/<slug>/(#374), a project's working tree should hold no teamai machine state. One file still lands in it:.teamai/managed-hooks.json, the index of team hooks teamai injected. Nothing ignores it..teamai/managed-hooks.json(the Copilot entry). It is the only file under.teamai/, and it shows as??.init .): every pull with team hooks writes it..teamai/.gitignoredoes not list it, sogit add -Acommits one member's hook index to the team's main branch.teamai packages installhas the same cause. It writes.teamai/teamai.lockinto the project, then creates.teamai/.gitignoreto hide it, and that file shows as??in a non-self project.Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed
HOME, local bare repos as remotes.Git project scope. The team repo has
hooks/hooks.yamlwith oneStophook andsharing.hooks.autoApply: true.~/.copilotexists..teamai/managed-hooks.jsonholds{"copilot": [...]}. The data home next to it already hasmanaged-main-checkout-hooks.json, so the Claude and Codex index has moved and the Copilot index has not.Self mode:
teamai init . --provider git --agent claude, then commit.teamai/hooks/hooks.yamlwith one hook and runteamai pull.Packages: the team
teamai.yamldeclarespackages.npm: [{name: left-pad, version: "1.3.0"}]. The project has apackage.json, and a stubnpmthat exits 0 is onPATH.Expected: none of these files in the working tree. Each belongs in the data home, as
managed-mcp.jsonandmanaged-main-checkout-hooks.jsonalready do.Cause
getManagedHooksPath(src/types.ts) builds the path fromgetTeamaiHome(scope, projectRoot), which is<projectRoot>/.teamaiin project scope, not fromgetDataHome. Its callers includereconcileTeamHooksForConfig(src/hooks.ts, the Copilot manifest) andresolveHookScope(src/types.ts, self mode), plushooks-cmd.tsanduninstall.ts.pkgInstalland the package status check (src/pkg/commands.ts) also usegetTeamaiHomeforteamai.lock.ensurePackageLockIgnored(src/pkg/manifest.ts) then writes.teamai/.gitignore.buildSelfModeGitignore(src/init.ts) liststeamai.lockandmanaged-mcp.json, but notmanaged-hooks.json.Impact
git add -Aor a commit-all from an IDE publishes one member's hook index to the team. It contains the rendered hook commands for that member's role and projects.git statusis never clean once Copilot or packages are in use.Proposed fix
getManagedHooksPathto the per-checkout workspace directory of the data home (workspaces/<id>/, wheremanaged-mcp.jsonalready lives), because the Copilot hook file and self-mode hooks are per checkout. Legacy readers (the pre-fix(hooks): unify hook-injection scope so project-scope init installs the SessionStart hook #370 Codex import, the legacy hook scope, uninstall) keep reading<root>/.teamai/managed-hooks.jsonthrough a separate helper. On the first pull after upgrade, move only thecopilotkey (project scope) or every key (self mode) out of the legacy file; delete the legacy file only when it is then empty and untracked, and havedoctorname a tracked one.teamai.lockto the data home and stop creating.teamai/.gitignorefor it in a non-self project. Move an existing.teamai/teamai.lock, and remove a.teamai/.gitignorethat holds only teamai's line.managed-hooks.jsontobuildSelfModeGitignore, and tomigrateSelfModeGitignorefor existing installs.Not checked
npm install: a stubnpmwas used, so the lock content reflects the stub.git rm --cachedit, or doctor should name it, when it is tracked.Environment
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider
git.5. In a repo without
.git/info/, pull says ".git/info is not writable" and withholds project MCP serversProblem
#886 keeps a project MCP config with resolved
${VAR}values out of git by adding it to.git/info/excludebefore writing it. Some repos have no.git/info/directory: those created withgit init --template=, with an emptyinit.templateDir, or by some tools. In such a repo, teamai reports that.git/info"is not writable" and withholds every team MCP server whose config holds a resolved value. The directory is not read-only. It does not exist, and teamai could create it.Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed
HOME, local bare team repo:mcp/mcp.yamlhas one server,secret-api, with the headerAuthorization: Bearer ${LAB_TOKEN}.env/env.yamlsetsLAB_TOKEN.sharing.mcp.autoApply: true.Afterwards,
.mcp.jsonand.git/infoare both absent. Aftermkdir .git/info && teamai pull --force, the server is written and.git/info/excludeholds the teamai block.Expected: teamai creates
.git/info/, adds/.mcp.jsontoexclude, and writes the server, with no warning.Cause
ensureExcludedFromGit(src/mcp-git-exclude.ts) checksfse.access(path.dirname(excludeFile), W_OK)before it writes.accesson a missing directory fails withENOENT, which the function reports as "not writable". The write that follows,writeFileAtomic, would have created the directory withensureDir.The fix hint, "add
/.mcp.jsonto it yourself", also points at the directory rather than the exclude file.Impact
deliveredblock, so every pull in such a repo would fail to exclude delivered resources with the same wrong reason.Proposed fix
In
ensureExcludedFromGit, check writability on the nearest existing ancestor of the exclude file, and let the write createinfo/. Report "not writable" only forEACCES,EPERMorEROFS. Name the exclude file, not its directory, in the hint.Add a test that removes
.git/infoand expects the server to be written and excluded.Not checked
info/. The exclude path comes fromgit rev-parse --git-path info/exclude, so the fix should be the same.info/in practice.Environment
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider
git, agent Claude.6. OpenCode V2 gets no team rules or team instructions: V2 ignores the
instructionsentries teamai relies onProblem
teamai delivers team rules and its instruction file to OpenCode by listing them in an OpenCode config's
instructionsarray:.opencode/opencode.jsongets the.opencode/rules/**/*.mdglob and.opencode/teamai-context.md.~/.config/opencode/opencode.jsongets the user rules globs and the absolute path of the instruction file.OpenCode V2 parses
instructionsand then ignores it. Members on OpenCode V2 get none of the team rules, and none of the culture,claudemd/or recall blocks.teamai doctorstill reports them as active.#982 added V2 support to teamai's OpenCode plugin, but only for hooks.
Evidence
Source-level, OpenCode v2.0.24 (tag
v2.0.24, commite7a34f0,github.com/anomalyco/opencode):packages/schema/src/config.tsdeclaresinstructionsas an optional array of strings.packages/core/src/config/plugin/instruction.tsis the only instruction source. It loads the globalAGENTS.mdand theAGENTS.mdin each ancestor directory, and nothing else. No code inpackages/readsinstructionsfrom a config document.grep -rn -E '(info|config|document|doc|entry)\??\.instructions' packagesreturns nothing.instructions.mdx, https://opencode.ai/v2/docs/instructions/) say: "The V2 config schema accepts aninstructionsarray, but V2 does not currently resolve its files, glob patterns, or URLs. This configuration does not add instructions to the model yet." They also say V2 "recognizesAGENTS.mdonly. It does not useCLAUDE.mdas a fallback."teamai side, built CLI 0.22.0 at f287dcb, sandboxed
HOMEwith~/.config/opencode:The installed plugin,
~/.config/opencode/plugin/teamai-hooks.ts, is built bybuildDualPluginDefinition(src/opencode-hooks.ts). Its V2setup(ctx)registerspromptandtool.execute.afterhooks and an event subscription. It adds no context to the system prompt.Cause
On V2, the only delivery channel teamai uses for rules and instructions is the
instructionsentry:opencodeProjectRuleGlobs, the user rules globs andopencodeContextReference(src/resources/opencode-config.ts). In user scope,opencodeClaudeFallbackrelies on OpenCode loading~/.claude/CLAUDE.md, which V2 does not do either.Impact
doctorreports success, so nothing points at the gap..opencode/opencode.jsonentries are the one OpenCode row that stays visible in a file the team may track. Delivering through the plugin would remove that row.Proposed fix
Deliver through the plugin teamai already installs in HOME. In its V2
setup(ctx), registerctx.session.hook('context', ...)and the same text on compaction. The plugin reads the files itself: fromctx.location.directoryit walks up to the nearest directory holding.opencode/teamai-context.mdor.opencode/rules/, and pushesteamai-context.mdfollowed by every.opencode/rules/**/*.md(the rules teamai delivered for the member's selection), sorted by path. In every directory it also pushes the user-scope rules and instruction file, as V1 does through the user config. When the user target is theCLAUDE.mdfallback, teamai also writes~/.config/opencode/teamai-context.mdfor V2.Keep the
instructionsentries for V1, which does resolve them. Havedoctorcheck the installed OpenCode major version and report V2 delivery from the plugin, not frominstructions.Not checked
contexthook reaches the model needs a model request, and none was made. Both the failure and the proposed hook rest on V2 source and docs.instructions. The docs call it "not currently" supported, andmigrate-v1.mdxlistsinstructionsas needing no migration, which contradicts them.packages/opencode/src/session/instruction.tsat v1.18.35). It was not run.Environment
macOS 27.0, Node.js 24.21.0, teamai 0.22.0 (f287dcb), OpenCode v2.0.24 (source and docs; the installed binary reports
opencode v2.0.24).7. In project scope, the co-author setting is written into the team's tracked
.claude/settings.jsonProblem
When the team sets
sharing.coAuthor.enabled: false, or a member setscoAuthorEnabled, teamai writesattributioninto the project's.claude/settings.json. It also writes to.codebuddy/settings.json, which the #915 delivery inventory observed, and to the settings of other Claude-family tools. Many teams track.claude/settings.json, so the first pull leaves it modified in every member's checkout. teamai also rewrites the whole file, which reformats it.Since #955 (PR #958), team hooks in project scope go to
.claude/settings.local.jsonto keep the shared file clean. The co-author reconcile still writes to the shared file.Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed
HOME, local bare team repo whoseteamai.yamlhassharing.coAuthor.enabled: false. The business repo tracks.claude/settings.json:{ "permissions": { "allow": ["Bash(npm test)"] } }{ "permissions": { - "allow": ["Bash(npm test)"] + "allow": [ + "Bash(npm test)" + ] + }, + "attribution": { + "commit": "", + "pr": "" } }Expected:
.claude/settings.jsonis unchanged. The setting goes to a file that is local to the member.Cause
resolveTargets(src/coauthor-reconcile.ts) targetspath.join(baseDir, paths.settings)for the Claude family, andbaseDiris the project root in project scope.applyClaudereads, patches and rewrites that file withwriteJson. Codex and Cursor are already skipped in project scope ("co-author is user-scope only"). The Claude family is not.Impact
coAuthorEnabledis a per-member override, so a member with a different setting rewrites the tracked file on their next pull.git statusclean. This file is shared content, so excluding it is not an option and the fix has to be here.Proposed fix
In project scope, write the Claude family's
attributiontosettings.local.json, the same personal layer #955 uses for team hooks. Do the same for CodeBuddy if it has a local settings layer. Otherwise skip it in project scope, as Codex and Cursor are skipped.On the next pull, remove an
attributionthat teamai wrote to the shared file before the fix, but only when it is teamai's{"commit": "", "pr": ""}andcoAuthorManagedrecords the file.Not checked
attributionfromsettings.local.json. Its settings documentation says local project settings override shared ones, but this was not run.claude-internal,tclaude, Qoder and WorkBuddy, which the [feat] keep the resources pull delivers in project scope out of git #915 inventory lists with the same writer. Not run.Environment
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider
git, agent Claude.8. Source skills go to the project's
.hermes/skillsand.openclaw/skills, which those tools never read, and to tools the member did not enableProblem
Skills from a team's
sources:take a different delivery path from the team's own skills, and they land in the wrong places:~/.hermes/skills,<openclaw workspace>/skills). Source skills go instead to<project>/.hermes/skills/and<project>/.openclaw/skills/, which neither tool reads. Hermes and OpenClaw members never receive source skills, and the project gets two dead copies.enabledAgents.Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed
HOMEwith~/.hermesand~/.openclaw/workspace, local bare repos.The team
teamai.yamlhassources: [{name: other, repo: <other-url>}]. The source repo publishesother-skill, and the team has the role skillfrontend/fe-skill.Output, with the sandbox
HOMEshortened to~:Then, with
enabledAgents: [claude, hermes, openclaw]:Expected:
other-skilllands wherefe-skilllands for each tool:~/.hermes/skills/and<openclaw workspace>/skills/.<project>/.hermes/or<project>/.openclaw/..cursor/, which is not enabled.Cause
pullSingleSource(src/source.ts) chooses its targets with its own loop overscopedToolPaths:ResourceHandler.isToolInstalled(toolPath.skills, baseDir), wherebaseDiris the project root, and then callsresolveSkillDestination.skillsDirForTool(src/resources/skills.ts), which sends Hermes to its home and OpenClaw to its workspace for the team's own skills.isAgentExcludedcheck, soenabledAgentsanddisabledAgentsdo not apply.Impact
.hermes/and.openclaw/copies that nothing loads. Agit add -Acommits them.Proposed fix
Resolve source skill targets through the same seam as team skills:
skillsDirForToolfor the directory, thenresolveSkillDestination, after anisAgentExcludedcheck.The source manifest records project-relative paths and rejects any path outside the project root, and the Hermes home and OpenClaw workspace are shared by every project subscribed to the same source. Record those destinations as absolute paths in the source manifest, owned per (source, project), and remove a copy there only when no other project's manifest for the same source still lists it, as team skills in those homes already work.
On the next pull, reclaim the copies an earlier pull left in
<project>/.hermes/skills/and<project>/.openclaw/skills/, using the source's install records. Reclaim a copy only when it equals the source skill at some revision of the source repo's history (the bug 12 proof); keep and name a changed one.Add a test that delivers one source skill and one team skill to every built-in tool and asserts that both land in the same directory.
Not checked
baseDiris HOME, so Hermes is probably right by accident and OpenClaw probably wrong (~/.openclaw/skillsinstead of the workspace). Not run.skills/directory. This draft relies on teamai's own routing for team skills (skillsDirForTool) and on the [bug] Team rules miss most tools: each tool's own rules format, else a file only it reads, else a hook or extension in project scope #946 research, not on running either tool.Environment
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider
git. Hermes and OpenClaw were not run; only their directories exist.9.
teamai init . --agent claudestill sets up every tool found in HOMEProblem
In single-repo mode,
--agentis meant to choose which tools teamai sets up in the repo. On main it has no effect.teamai init . --agent claudestill:enabledAgents.Every later pull then delivers skills and hooks to those tools as well. The extra hook files are never committed, so they stay as untracked files in the team's repo.
Reproduction
Built CLI 0.22.0 at f287dcb. Sandboxed
HOMEwith~/.claude,~/.codex,~/.cursor,~/.codebuddyand~/.copilot. Repoself, whoseoriginis a local bare repo.init .requires an https origin, so the lab exposed it as one.The
[bootstrap]line, and the hook writes before "Local config saved", come from the clone-time self-heal, not frominit. After a team hook is added and a pull runs,.codex/skills/,.cursor/skills/,.codebuddy/skills/and.github/skills/also receive theteamaiskill.Expected:
enabledAgents: [claude]..claude/settings.json, whichinitcommits.Cause
initSelfRepo(src/init.ts) writes.teamai/teamai.yamlwithmode: self. Only later does it callloadLocalConfigForScope('project', businessRepoRoot)to read the existing config.With
mode: selfon disk and no config yet, that load runs the clone-time self-heal:readConfigFrom→bootstrapSelfRepo(src/config.ts,src/bootstrap.ts). The self-heal setsenabledAgentstodetectHomeInstalledAgents(), injects hooks for all of them and saves the config.initSelfRepothen takes the union of that "existing" config and--agent, so the filter never applies.PR #852 diagnosed the same cause ("Bug 2") and fixed it by reading the config once, before
teamai.yamlis written. It was closed without merging. The dry-run half was later handled by refusinginit --dry-run(#900). The--agenthalf is still open.Impact
--agentdoes nothing in single-repo mode whenever other tools are installed in HOME.git add -Acommits them.initalso runs a full self-heal as a side effect, including registering git hooks and Codex trust, before its own steps.Proposed fix
Read the pre-init project config once, before
teamai.yamlis written, and use that snapshot for every later step:inheritUserScope, theenabledAgentsunion, the tool-roots carry-over andsettleModeSwitch. This is #852's approach.Alternatively, give
loadLocalConfigForScopea no-self-heal option forinit. Either way, add a test that runsinit . --agent claudewith other tools in HOME and assertsenabledAgents: [claude]and no other tool's files.Not checked
--agent, with a TTY). It probably merges in the same way.init .on a repo that already has a config. There the load returns the real config and the self-heal does not run.Environment
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider
git, single-repo mode.10. CodeBuddy reads only the first user MCP file that exists, so teamai's servers and the member's own servers hide each other
Problem
In user scope, CodeBuddy reads exactly one MCP file: the first that exists of
~/.codebuddy/.mcp.json,~/.codebuddy/mcp.jsonand~/.codebuddy.json. It does not merge them. teamai always writes the second one,~/.codebuddy/mcp.json. So:codebuddy mcp add -s userhas~/.codebuddy/.mcp.json. teamai's team servers are then never loaded, and nothing says so.~/.codebuddy.jsonloses them as soon as teamai creates~/.codebuddy/mcp.json.Reproduction
CodeBuddy Code 2.161.4, sandboxed
HOME.~/.codebuddy/mcp.jsonpresent, the member'susr-srvin~/.codebuddy.jsondisappears fromcodebuddy mcp list.~/.codebuddy/.mcp.jsonpresent, teamai'stm-userdisappears.codebuddy mcp add -s user my-user ...creates~/.codebuddy/.mcp.json; after that, teamai'stm-useris no longer listed.Servers in the local scope (
~/.codebuddy.json→projects[...]) are unaffected.The CodeBuddy docs state the rule: "The system does not merge multiple configuration files within the same scope; it only uses the first existing file." (https://www.workbuddy.ai/docs/cli/mcp)
Cause
toolPaths.codebuddy.mcp(src/types.ts) is fixed to.codebuddy/mcp.json, the second candidate in CodeBuddy's lookup order.Impact
Team MCP servers silently fail to load for members who use CodeBuddy's own
mcp add, or teamai silently hides the member's existing servers.doctorreports the team servers as delivered in both cases.Proposed fix
In user scope, write to the first CodeBuddy user MCP file that exists, and only create
~/.codebuddy/.mcp.json(CodeBuddy's recommended file) when none exists. Havedoctorwarn when the file teamai writes is not the one CodeBuddy reads. WorkBuddy is not affected: its app reads~/.workbuddy/mcp.json, the file teamai writes (confirmed in the app).Not checked
~/.workbuddy/.mcp.jsonshadows~/.workbuddy/mcp.jsonin the WorkBuddy app.~/.codebuddy/mcp.jsonwhen the member also has~/.codebuddy/.mcp.json.Environment
macOS 27.0, CodeBuddy Code 2.161.4, teamai 0.22.0 (f287dcb).
11. Withdrawn: WorkBuddy's project MCP servers go to
.workbuddy/mcp.json, which the WorkBuddy engine does not read (needs a check in the app)Problem
In project scope teamai writes WorkBuddy's team MCP servers to
<project>/.workbuddy/mcp.json(toolPaths.workbuddy.mcpProjectinsrc/types.ts;docs/usage-guide.md). The engine WorkBuddy runs (the CodeBuddy engine) reads project servers only from<project>/.mcp.jsonor<project>/mcp.json. If the desktop app does not read.workbuddy/mcp.jsonitself, WorkBuddy members get no team project servers from it.Evidence
CODEBUDDY_CONFIG_DIR=WORKBUDDY_CONFIG_DIR=$HOME/.workbuddy), with.mcp.json(proj-srv) and.workbuddy/mcp.json(in-wb-mcp) in the project:codebuddy mcp listshowsproj-srv, the user servers in~/.workbuddy/mcp.jsonand the local ones in~/.workbuddy/.codebuddy.json, but notin-wb-mcp.<project root>/.mcp.jsonand<project root>/mcp.jsonfor project scope (https://www.workbuddy.ai/docs/cli/mcp). The official WorkBuddy MCP guide gives no file paths (https://www.workbuddy.ai/docs/workbuddy/From-Beginner-to-Expert-Guide/Function-Description/MCP-Guide). A third-party article names<project>/.workbuddy/mcp.jsonas the project level, citing that guide, without testing it (https://blog.sandbase.ai/workbuddy-mcp-setup-permissions-2026/)._workbuddyMcpOrigin: "custom"), so the app could read.workbuddy/mcp.jsonitself. That was not checked.Check needed (one session with the WorkBuddy app)
<project>/.workbuddy/mcp.json, open the project, and see whether it is listed.~/.workbuddy/.codebuddy.jsonunderprojects["<opened folder realpath>"].mcpServersand see whether it is listed.If check 1 fails, this bug is confirmed. If check 2 passes, the fix is to deliver WorkBuddy's project servers to that local scope, keyed by the realpath of each worktree root (the key CodeBuddy uses), which also keeps them out of the repo.
Impact (if confirmed)
WorkBuddy-only members get no team project MCP servers; members who also have Claude or CodeBuddy get them only through
.mcp.json.doctorreports them as delivered.Environment
macOS 27.0, CodeBuddy Code 2.161.4 (engine), teamai 0.22.0 (f287dcb). WorkBuddy desktop not run.
12. Without a delivery record, pull overwrites a member's own skill, rule, agent, doc or MCP server, and duplicates its own hook entries
Added 2026-10-07 after a review of the #915 plan.
Problem
When a project tool folder already holds a file or directory at the path where teamai delivers a team resource, or an MCP config already holds a server with the same name as a team server, and the checkout has no delivery record for it,
pullandinitreplace it with the team version without a word. The output reports the resource as synced.It hits a member who wrote a skill, rule or agent before joining the team, a resource installed by another tool or by hand, a member's own MCP server that happens to share a team server's name, and any copy whose record was lost (bug 1, bug 2). Bug 3 is the Codex
.agents/skillscase of the same failure.Reproduction
Built CLI 0.22.0 at f287dcb, sandboxed
HOME, local bare team repo with a skillteam-skill, a ruleteam-rule, an agentteam-agent, a docguide.md, aclaudemd/fragment, an MCP serverplain-apiand aStopteam hook. Beforeinit, the business repo holds the member's own files at the same paths and names:plain-apiteamai-context.mdA second run shows the other side of the same cause. With teamai's main-checkout hook manifest removed (as when the record is lost),
teamai pull --forceadds a second, identical[teamai:hook:team-stop]entry to.claude/settings.local.jsonnext to the one it wrote before, so the team hook runs twice. The usage guide states that "unrecorded or ambiguous legacy team-hook copies are preserved", and a new copy is then written.Expected: every resource behaves like
teamai-context.md: the member's file or server is left alone and named, and the team version is not delivered there until the member resolves it.Cause
Ownership without a record defaults to teamai:
SkillsHandler.pullItemand the agents handler skip a destination only whenkeepsEditedCopysays so;classifyCopy(src/resources/delivered-copies.ts) keeps a copy only when a file has a recorded hash. A destination with no record is always written.judgeCopy); bug 2 is the lost-record case of it.DocsHandler.pullDocswrites the mirror without checking an existing file it never wrote.The safe cases already prove ownership in the file:
teamai-contexttargets check teamai's markers (planFile), and hooks write marked entries next to the member's.Impact
A member's skill, rule, agent, doc or MCP server is lost the first time the team publishes one with the same name, or on the first pull after the record is lost. Only the MCP case prints anything, and it reads as a normal change.
Proposed fix
One ownership rule for every resource without a record, the one bug 2 needs too:
doctorlists it.Not checked
.github/copilot-instructions.mdand theCLAUDE.mdblocks of the internal Claude variants (they use marked blocks, liketeamai-context).settings.local.json; Codexhooks.jsonand other hook files follow the same manifest logic and were not run.Environment
macOS 27.0, Node.js 24.21.0, git 2.55.0, teamai 0.22.0 (f287dcb), provider
git, agent Claude.