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
In project scope, pull writes team resources into the business repo's working tree, and nothing keeps git away from them. Once #952, #957 and #958 land, a project pull leaves this:
<business repo>/ git status after pull
├── .claude/skills/<name>/ ?? team and source skills (#929)
├── .claude/agents/<file> ??
├── .claude/rules/<rule>.md ?? selected by the member's roles
├── .claude/rules/teamai-context.md ?? this member's culture, claudemd/ and recall (#952)
├── .claude/settings.local.json ?? this member's team hooks, main checkout (#958)
├── .codex/skills/<name>/ ??
├── .codex/hooks.json ?? team hooks (#958)
├── .cursor/rules/teamai-context.mdc ??
└── .teamai/docs/ ?? docs mirror
One git add -A commits all of it. The member's role selection then reaches every teammate, and the committed copy drifts from the team repo. Doing it by hand doesn't scale:
Folder patterns (.claude/) hide what the team commits there on purpose.
In self mode it breaks delivery. Ignored .teamai/env never pushes (src/resources/env.ts:266), and init . silently skips ignored hook settings (src/utils/git.ts:313-324).
What this closes, with the other PRs
flowchart LR
subgraph merged [merged]
m886["#886 MCP exclude block"]
m940["#940 #947 Codex rules via hooks"]
m929["#929 source install records"]
end
subgraph pending [waiting for review]
p952["#952 per-member instruction files"]
p957["#957 draft: rules per tool channel"]
p958["#958 project hooks across worktrees"]
p964["#964 sync before the session"]
p956["#956 Codex project MCP"]
end
t915["#915 keep delivered paths out of git"]
m886 -- "mechanism" --> t915
m929 -- "source skill paths" --> t915
m940 --> p952
p952 --> p957
p957 -- "rules paths" --> t915
p952 -- "teamai-context paths" --> t915
p958 -- "settings.local.json" --> t915
p964 -. "new worktrees ready at start" .-> t915
p956 -. "MCP row" .-> t915
classDef done fill:#2f8f4f,stroke:#1f6b39,color:#fff;
classDef open fill:#b5762f,stroke:#865520,color:#fff;
classDef draft fill:#8a8a8a,stroke:#5c5c5c,color:#fff,stroke-dasharray:4 3;
classDef target fill:#3b5f99,stroke:#2a4573,color:#fff;
class m886,m940,m929 done;
class p952,p958,p964,p956 open;
class p957 draft;
class t915 target;
only the copies delivered into tool folders; never .teamai/
Proposed Solution
When pull delivers a resource into a project scope, it adds the path to the repo's .git/info/exclude. #886 (for #882, merged) already does this for MCP configs that hold resolved tokens and for local-agent credentials: a marked block, local to the clone, that commits nothing and is idempotent. This extends that block to what pull delivers.
pull (project scope, self mode included)
deliver skills, rules, agents, instruction files, hooks
pull source skills
+ sync the teamai block in .git/info/exclude with the delivered paths of every live checkout:+ .<tool>/skills/<name>/ <rules dir>/<file> .<tool>/agents/<file>+ the teamai-context files (#952) .claude/settings.local.json (#958)+ leave out any path git already tracks, and let doctor name it
Path
Kind
Excluded
.<tool>/skills/<name>/ (team and source skills), .<tool>/agents/<file>
MCP configs: with a resolved value (#886's block); without one (.mcp.json, .codex/config.toml from #956, ...)
already; no
.teamai/docs/
whole directory
not yet, see below
AGENTS.md, .omp/AGENTS.md, blocks in .claude/CLAUDE.md and .codebuddy/CODEBUDDY.md (#952); .codex/rules (#940); .pi/rules, .openclaw/rules, .openclaw/workspace/AGENTS.md, a project's .workbuddy/rules (#957)
no longer written
nothing to do
It is off by default, like sharing.recall.enabled (src/types.ts:83), so existing teams see no change. init turns it on in the teamai.yaml it writes for a new team repo (src/init.ts:1801-1805). A member can override it locally. While it is off, doctor names the delivered paths that are untracked, so a team can find the option.
# teamai.yamlsharing:
gitExclude:
enabled: true # written by init for new teams; the default is false; members can override locally
Docs stay visible for now. The agents' search tools skip excluded files: Claude Code's Grep, Codex's rg and file search, OpenCode's grep and glob, Cursor's search. Reading by path still works. Excluding .teamai/docs/ would make the team docs invisible to search. They join the block once a search with an explicit .teamai/docs path is verified in each tool and the core skill tells agents to use it. #374's R1 stays open until then.
Edge cases
It works per item, not per folder. A team's own .claude/settings.json, its repo-specific skills and rules, and its AGENTS.md stay visible to git.
Files where teamai manages entries next to the team's content stay visible, even when teamai created them and they hold only its entries. If the team later adds its own settings there, an excluded file would never be committed, and nothing would warn about it.
In self mode the source is .teamai/, committed on purpose (feat: 让项目级 .teamai/ 跟着 git 仓库走(clone 即完成项目初始化) #198), and init . also commits the tool settings that carry the hooks. Neither is excluded. The copies pull delivers from .teamai/ into the tool folders are excluded as in project scope: repo.localPath is <repo>/.teamai and projectRoot is the repo root (src/init.ts:1224-1228).
exclude has no effect on a path git already tracks. doctor names such a path and changes nothing. git add -f still commits a delivered file on purpose.
One block covers every checkout, because worktrees read info/exclude from the common git dir. Checked: a linked worktree reports the main repo's path from git rev-parse --git-path info/exclude, and a listed path does not show in its git status. Checkouts can differ in their tool folders (fix(pull): deliver team resources to a worktree added after the last pull (#807) #811), so pull passes the union of the delivery records of every checkout git worktree list still reports. A pull in one worktree never drops another's lines.
Hooks: built-in hooks stay in HOME (docs/usage-guide.md:663). fix(hooks): trust Codex hooks and share project hooks across worktrees #958 moves the team's own Claude Code and Codex hooks to the main checkout; the table covers both files. The other channels (Codex, ZCode and DeepSeek Harness hooks, the Pi and Oh My Pi extensions, the Hermes plugin) put nothing in the repo.
Do the tools still load excluded files?
Yes, for every tool checked. Exclude patterns must avoid two shapes.
Tool
Loads skills, rules, agents
Evidence
Claude Code 2.1.287
yes, nested rules included
live, per-item and whole-folder excludes
Codex CLI 0.160.0
yes
live with per-item excludes; source (read_dir)
OpenCode 1.18.34
yes, instructions globs included
live, /.opencode/ excluded; source (npm glob)
Copilot CLI 1.0.90
yes, .github/instructions and .claude
live, per-item and whole-folder
Cursor CLI 2026.09.28
yes, with the pattern rule below
source and its bundled rg; not live
Cursor IDE, VS Code Copilot, Kiro, CodeBuddy
not checked
Cursor lists rules with rg started inside .cursor/rules. An excluded subdirectory there (/.cursor/rules/frontend/, /.cursor/rules/*) silently drops the nested rules.
When an OpenCode skill runs, OpenCode lists its files with rg. /<skill>/ keeps them; /<skill>/* hides them.
Alternatives considered
Exclude whole tool folders (.claude/, .codex/). It is simpler, but it hides what teams commit there on purpose, and a new file the team adds would never be committed, with nothing to warn about it.
On by default for every team. It would silently change what git shows for teams that commit the delivered files on purpose, and their committed copy would stop picking up new resources.
Off for everyone, with no init default. Nothing changes for anyone, but every team would have to discover the option.
One sub-block per checkout. It fixes the union too, but it fills the member's file with a block per worktree.
Implementation shape
#886 already solves the hard parts in src/mcp-git-exclude.ts: finding info/exclude through the common git dir, a marked block that never takes the member's lines, a lock and atomic write, a damaged block, an ignore rule that re-includes a path, tracked paths, and --dry-run. Today they sit next to the MCP rules (carriesResolvedValue, carriesLocalAgentCredential), and six callers (mcp-reconcile, mcp-cmd, local-agent, mcp-resolved-files, doctor-delivery, uninstall) combine its 19 exports themselves. A small git-exclude module owns the mechanism, with one block per owner:
sync(repo, owner, paths) the owner's block holds exactly these paths; tracked paths are left out and reported
remove(repo, owner?) uninstall
report(repo) doctor: listed and tracked paths per owner
flowchart LR
subgraph callers [callers]
pull["pull<br/>union over live checkouts of:<br/>handlers' desired sets,<br/>resolveInstructionTargets (#952),<br/>source install records (#929),<br/>settings.local.json (#958)"]:::added
mcp["mcp-reconcile, mcp-cmd,<br/>local-agent (#886)"]:::changed
un[uninstall]:::changed
doc[doctor]:::changed
end
mcprule["mcp-git-exclude.ts<br/>keeps only: which MCP files<br/>carry a resolved value or credential"]:::changed
subgraph ge ["git-exclude.ts: one seam"]
api["sync(repo, owner, paths)<br/>remove(repo, owner?)<br/>report(repo)"]:::added
hidden["hidden inside:<br/>git-path via the common dir<br/>one marked block per owner<br/>lock + atomic write<br/>damaged block, re-including rule<br/>tracked paths, dry run"]:::added
end
file[(".git/info/exclude<br/>block teamai:mcp<br/>block teamai:delivered<br/>member lines untouched")]
pull -- "owner delivered, after pullSources<br/>only if gitExclude.enabled<br/>never .teamai/ in self mode" --> api
mcp --> mcprule
mcprule -- "owner mcp<br/>always: security" --> api
un -- "remove every owner" --> api
doc -- "report per owner;<br/>untracked delivered paths while off" --> api
api --- hidden
hidden --> file
classDef added fill:#2f8f4f,stroke:#1f6b39,color:#fff;
classDef changed fill:#b5762f,stroke:#865520,color:#fff;
Loading
The two owners keep separate policies. The MCP block always applies, because it protects tokens. The delivered block follows the flag, and in self mode it never lists a path under .teamai/. pull builds the union from the handlers' desired sets, #952's resolveInstructionTargets (src/instruction-targets.ts), #929's per-destination install records and #958's main-checkout hooks, and calls sync once, after pullSources (src/pull.ts:2430-2445). No resource type and no tool needs exclude code of its own. Legacy name-only installed.json entries cannot prove a path, so they stay out.
Patterns: one line per file, or /<path>/ for a whole skill. Never /<dir>/*, and never a directory inside a rules directory.
Acceptance: an e2e that pulls with the flag on and asserts git check-ignore for each delivered path, that no line ends in /*, and that no line names a directory under a rules directory.
Sequencing: after #952, #957 and #958, which move or add delivered paths. #964 makes excluded resources present at the first session, a new worktree's included.
The claim that a manual .gitignore breaks nothing for updates in project scope, and that excluded copies survive git clean -fd and git stash -u, comes from reading src/pull.ts:1186-1205 and docs/usage-guide.md:661. It has not been run.
Self mode is checked by reading the code only. In the sandbox, init . rejected the local remote. The implementation needs a real-CLI run of init . and pull in self mode.
Not checked: the skills and agents paths of the other agents, Qoder CN, Pi's project hook paths, and the tools marked "not checked" above. The implementation needs a real-CLI run for each before it relies on their paths.
Problem
In project scope,
pullwrites team resources into the business repo's working tree, and nothing keeps git away from them. Once #952, #957 and #958 land, a project pull leaves this:One
git add -Acommits all of it. The member's role selection then reaches every teammate, and the committed copy drifts from the team repo. Doing it by hand doesn't scale:.gitignoreis committed and shared by the whole team. It has to list the union of every member's tools and roles, and fix(pull): deliver each member's team instructions without the shared AGENTS.md (#945) #952, fix(rules): deliver team rules through each tool's own channel #957, fix(hooks): trust Codex hooks and share project hooks across worktrees #958 and fix(mcp): deliver team MCP servers to Codex in project scope (#954) #956 each add paths someone must edit in by hand..claude/) hide what the team commits there on purpose..teamai/envnever pushes (src/resources/env.ts:266), andinit .silently skips ignored hook settings (src/utils/git.ts:313-324).What this closes, with the other PRs
flowchart LR subgraph merged [merged] m886["#886 MCP exclude block"] m940["#940 #947 Codex rules via hooks"] m929["#929 source install records"] end subgraph pending [waiting for review] p952["#952 per-member instruction files"] p957["#957 draft: rules per tool channel"] p958["#958 project hooks across worktrees"] p964["#964 sync before the session"] p956["#956 Codex project MCP"] end t915["#915 keep delivered paths out of git"] m886 -- "mechanism" --> t915 m929 -- "source skill paths" --> t915 m940 --> p952 p952 --> p957 p957 -- "rules paths" --> t915 p952 -- "teamai-context paths" --> t915 p958 -- "settings.local.json" --> t915 p964 -. "new worktrees ready at start" .-> t915 p956 -. "MCP row" .-> t915 classDef done fill:#2f8f4f,stroke:#1f6b39,color:#fff; classDef open fill:#b5762f,stroke:#865520,color:#fff; classDef draft fill:#8a8a8a,stroke:#5c5c5c,color:#fff,stroke-dasharray:4 3; classDef target fill:#3b5f99,stroke:#2a4573,color:#fff; class m886,m940,m929 done; class p952,p958,p964,p956 open; class p957 draft; class t915 target;git status, whatever tools they useCLAUDE.mdandAGENTS.md, so diffs collideteamai-contextfile per member), #957git statusin every checkoutgit pull), #865 (merged, keeps edits)git clean -fdandgit stash -u.codex/config.toml.teamai/is committed on purpose (#198).teamai/Proposed Solution
When
pulldelivers a resource into a project scope, it adds the path to the repo's.git/info/exclude. #886 (for #882, merged) already does this for MCP configs that hold resolved tokens and for local-agent credentials: a marked block, local to the clone, that commits nothing and is idempotent. This extends that block to whatpulldelivers..<tool>/skills/<name>/(team and source skills),.<tool>/agents/<file>.claude/rules,.cursor/rules,.github/instructions,.kiro/steering,.qoder/rules,.codebuddy/rules(also WorkBuddy, #957),.joycode/rules,.omp/rules,.opencode/rules.claude/rules/teamai-context.md,.cursor/rules/teamai-context.mdc,.codebuddy/rules/teamai-context.md,.opencode/teamai-context.md(#952).claude/settings.local.json(#958).github/copilot-instructions.md,.claude-internal/CLAUDE.md,.tclaude/CLAUDE.md(blocks);.opencode/opencode.json(instructionsentries);.codex/hooks.json(#958).mcp.json,.codex/config.tomlfrom #956, ...).teamai/docs/AGENTS.md,.omp/AGENTS.md, blocks in.claude/CLAUDE.mdand.codebuddy/CODEBUDDY.md(#952);.codex/rules(#940);.pi/rules,.openclaw/rules,.openclaw/workspace/AGENTS.md, a project's.workbuddy/rules(#957)It is off by default, like
sharing.recall.enabled(src/types.ts:83), so existing teams see no change.initturns it on in theteamai.yamlit writes for a new team repo (src/init.ts:1801-1805). A member can override it locally. While it is off,doctornames the delivered paths that are untracked, so a team can find the option.Docs stay visible for now. The agents' search tools skip excluded files: Claude Code's Grep, Codex's
rgand file search, OpenCode's grep and glob, Cursor's search. Reading by path still works. Excluding.teamai/docs/would make the team docs invisible to search. They join the block once a search with an explicit.teamai/docspath is verified in each tool and the core skill tells agents to use it. #374's R1 stays open until then.Edge cases
.claude/settings.json, its repo-specific skills and rules, and itsAGENTS.mdstay visible to git.teamai-contextfiles are whole files teamai owns. fix(pull): deliver each member's team instructions without the shared AGENTS.md (#945) #952 keeps and reports a same-named file teamai does not own; that file is not listed..github/copilot-instructions.mdand.codex/hooks.jsonhold the member's selection next to content the team may track.excludecannot help there. [bug] Team members with different roles overwrite each other's blocks in shared root AGENTS.md #945 keeps the Copilot target on purpose and names the risk..teamai/, committed on purpose (feat: 让项目级 .teamai/ 跟着 git 仓库走(clone 即完成项目初始化) #198), andinit .also commits the tool settings that carry the hooks. Neither is excluded. The copiespulldelivers from.teamai/into the tool folders are excluded as in project scope:repo.localPathis<repo>/.teamaiandprojectRootis the repo root (src/init.ts:1224-1228).excludehas no effect on a path git already tracks.doctornames such a path and changes nothing.git add -fstill commits a delivered file on purpose.info/excludefrom the common git dir. Checked: a linked worktree reports the main repo's path fromgit rev-parse --git-path info/exclude, and a listed path does not show in itsgit status. Checkouts can differ in their tool folders (fix(pull): deliver team resources to a worktree added after the last pull (#807) #811), sopullpasses the union of the delivery records of every checkoutgit worktree liststill reports. A pull in one worktree never drops another's lines..pi/rules,.openclaw/rules, nested Kiro rules) are no longer teamai's, so they leave the block and show ingit status.docs/usage-guide.md:663). fix(hooks): trust Codex hooks and share project hooks across worktrees #958 moves the team's own Claude Code and Codex hooks to the main checkout; the table covers both files. The other channels (Codex, ZCode and DeepSeek Harness hooks, the Pi and Oh My Pi extensions, the Hermes plugin) put nothing in the repo.Do the tools still load excluded files?
Yes, for every tool checked. Exclude patterns must avoid two shapes.
read_dir)instructionsglobs included/.opencode/excluded; source (npmglob).github/instructionsand.clauderg; not livergstarted inside.cursor/rules. An excluded subdirectory there (/.cursor/rules/frontend/,/.cursor/rules/*) silently drops the nested rules.rg./<skill>/keeps them;/<skill>/*hides them.Alternatives considered
.claude/,.codex/). It is simpler, but it hides what teams commit there on purpose, and a new file the team adds would never be committed, with nothing to warn about it..gitignore. It changes a file the team owns, on every member's machine, which is the reason [feat] keep project MCP configs with resolved tokens out of git #882 gives forinfo/exclude.Implementation shape
#886 already solves the hard parts in
src/mcp-git-exclude.ts: findinginfo/excludethrough the common git dir, a marked block that never takes the member's lines, a lock and atomic write, a damaged block, an ignore rule that re-includes a path, tracked paths, and--dry-run. Today they sit next to the MCP rules (carriesResolvedValue,carriesLocalAgentCredential), and six callers (mcp-reconcile,mcp-cmd,local-agent,mcp-resolved-files,doctor-delivery,uninstall) combine its 19 exports themselves. A smallgit-excludemodule owns the mechanism, with one block per owner:flowchart LR subgraph callers [callers] pull["pull<br/>union over live checkouts of:<br/>handlers' desired sets,<br/>resolveInstructionTargets (#952),<br/>source install records (#929),<br/>settings.local.json (#958)"]:::added mcp["mcp-reconcile, mcp-cmd,<br/>local-agent (#886)"]:::changed un[uninstall]:::changed doc[doctor]:::changed end mcprule["mcp-git-exclude.ts<br/>keeps only: which MCP files<br/>carry a resolved value or credential"]:::changed subgraph ge ["git-exclude.ts: one seam"] api["sync(repo, owner, paths)<br/>remove(repo, owner?)<br/>report(repo)"]:::added hidden["hidden inside:<br/>git-path via the common dir<br/>one marked block per owner<br/>lock + atomic write<br/>damaged block, re-including rule<br/>tracked paths, dry run"]:::added end file[(".git/info/exclude<br/>block teamai:mcp<br/>block teamai:delivered<br/>member lines untouched")] pull -- "owner delivered, after pullSources<br/>only if gitExclude.enabled<br/>never .teamai/ in self mode" --> api mcp --> mcprule mcprule -- "owner mcp<br/>always: security" --> api un -- "remove every owner" --> api doc -- "report per owner;<br/>untracked delivered paths while off" --> api api --- hidden hidden --> file classDef added fill:#2f8f4f,stroke:#1f6b39,color:#fff; classDef changed fill:#b5762f,stroke:#865520,color:#fff;The two owners keep separate policies. The MCP block always applies, because it protects tokens. The delivered block follows the flag, and in self mode it never lists a path under
.teamai/.pullbuilds the union from the handlers' desired sets, #952'sresolveInstructionTargets(src/instruction-targets.ts), #929's per-destination install records and #958's main-checkout hooks, and callssynconce, afterpullSources(src/pull.ts:2430-2445). No resource type and no tool needs exclude code of its own. Legacy name-onlyinstalled.jsonentries cannot prove a path, so they stay out.Patterns: one line per file, or
/<path>/for a whole skill. Never/<dir>/*, and never a directory inside a rules directory.Acceptance: an e2e that pulls with the flag on and asserts
git check-ignorefor each delivered path, that no line ends in/*, and that no line names a directory under a rules directory.Sequencing: after #952, #957 and #958, which move or add delivered paths. #964 makes excluded resources present at the first session, a new worktree's included.
Verification and what is not checked
claude,codex, and a team repo with one skill, one rule, one agent, one doc and oneclaudemd/fragment, read withgit status --porcelain -uallafterpull. The tree above is that run updated from the descriptions of fix(rules): give Codex the team rules: session hooks in a project, its own AGENTS.md in user scope #940, fix(pull): deliver each member's team instructions without the shared AGENTS.md (#945) #952, fix(rules): deliver team rules through each tool's own channel #957 and fix(hooks): trust Codex hooks and share project hooks across worktrees #958, not a new run..gitignorebreaks nothing for updates in project scope, and that excluded copies survivegit clean -fdandgit stash -u, comes from readingsrc/pull.ts:1186-1205anddocs/usage-guide.md:661. It has not been run.init .rejected the local remote. The implementation needs a real-CLI run ofinit .andpullin self mode.Open questions: