Skip to content

[feat] keep the resources pull delivers in project scope out of git #915

Description

@SaulMoro

Problem

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:

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;
Loading
Scenario Today Fixed by What #915 adds
Members on different AI tools in one repo each tool's copies land in the tree; Codex had no project rules #940, #947 (merged); #952, #957, #956 every member's delivered items stay out of git status, whatever tools they use
Members with different roles role blocks go into the shared CLAUDE.md and AGENTS.md, so diffs collide #952 (one teamai-context file per member), #957 those files, role-selected rules and hooks can't be committed and handed to others
First AI session: after init, in a new worktree, after a team change the tool reads its config before the SessionStart pull lands, so rules, MCP and team hooks are missing or stale (#963) #964 (pull before the session); #811 (merged), #958 for worktrees one block in the common git dir keeps what #964 prepares out of git status in every checkout
Receiving team updates a committed copy shows a diff after each pull #964 (pull after git pull), #865 (merged, keeps edits) nothing delivered is committed, so a pull never dirties git; excluded copies survive git clean -fd and git stash -u
Team repo duplicated into N projects each project can commit its own copy, third-party source skills included #929 (merged, per-destination install records) delivered copies, source skills included, stay local
Secrets in MCP configs resolved tokens could be committed #886 (merged); #956 adds .codex/config.toml nothing new; it shares #886's mechanism
Self mode .teamai/ is committed on purpose (#198) 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> whole item yes
each tool's rules files: .claude/rules, .cursor/rules, .github/instructions, .kiro/steering, .qoder/rules, .codebuddy/rules (also WorkBuddy, #957), .joycode/rules, .omp/rules, .opencode/rules whole file, one line each yes
.claude/rules/teamai-context.md, .cursor/rules/teamai-context.mdc, .codebuddy/rules/teamai-context.md, .opencode/teamai-context.md (#952) whole file, the member's selection yes
.claude/settings.local.json (#958) personal by Claude Code convention; holds the member's team hooks yes
.github/copilot-instructions.md, .claude-internal/CLAUDE.md, .tclaude/CLAUDE.md (blocks); .opencode/opencode.json (instructions entries); .codex/hooks.json (#958) teamai entries next to the team's no
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.yaml
sharing:
  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.
  • The teamai-context files 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.
  • 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.
  • .github/copilot-instructions.md and .codex/hooks.json hold the member's selection next to content the team may track. exclude cannot 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.
  • 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.
  • Edited legacy copies that fix(rules): deliver team rules through each tool's own channel #957 keeps (.pi/rules, .openclaw/rules, nested Kiro rules) are no longer teamai's, so they leave the block and show in git status.
  • 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.
  • Write the paths into the committed .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 for info/exclude.
  • 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.
  • A second exclude module next to feat(mcp): keep project MCP configs with resolved tokens out of git (#882) #886's. Two copies of the block, lock and worktree logic would drift apart.
  • 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.

Verification and what is not checked

Open questions:

  • Should a later major version turn it on by default for every team?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions