Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ed103c4
fix(hooks): trust teamai's Codex hooks and keep project team hooks in…
SaulMoro Oct 1, 2026
6b2d190
fix(hooks): trust main-checkout Codex hooks before worktree sessions
SaulMoro Oct 1, 2026
2c71bd4
fix(hooks): close Codex app-server after initialization failure
SaulMoro Oct 1, 2026
a18899f
test(hooks): mock Git worktree boundaries in unit fixtures
SaulMoro Oct 1, 2026
fd87781
fix(hooks): trust Codex built-ins after unresolved team-hook fallback
SaulMoro Oct 1, 2026
a77c525
fix(hooks): preserve project isolation in removal and e2e (#955)
SaulMoro Oct 2, 2026
b177c7a
test(hooks): retain main-checkout isolation with host guard (#955)
SaulMoro Oct 2, 2026
9ae9168
fix(hooks): track exact Codex hook ownership (#955)
SaulMoro Oct 2, 2026
79b01c8
fix(hooks): honor bare workspaces and configured project paths (#955)
SaulMoro Oct 2, 2026
c7d0a46
fix(hooks): recover legacy Codex options and orphaned project hooks (…
SaulMoro Oct 2, 2026
a9ab3f2
fix(hooks): migrate pre-370 Codex hook ownership (#955)
SaulMoro Oct 2, 2026
b928c3b
fix(hooks): limit main-checkout cleanup to owned entries (#955)
SaulMoro Oct 2, 2026
8042fbb
fix(hooks): match Claude main-checkout ownership by hook id (#955)
SaulMoro Oct 2, 2026
92d89ff
Merge origin/main into fix/955-codex-hook-trust
SaulMoro Oct 3, 2026
8930a42
fix(hooks): refresh main-checkout hooks without HOME roots (#955)
SaulMoro Oct 3, 2026
121b8cb
Merge origin/main into fix/955-codex-hook-trust
SaulMoro Oct 3, 2026
0f57f13
fix(hooks): trust written Codex hooks after Git-hook failure (#955)
SaulMoro Oct 3, 2026
bc87a65
fix(bootstrap): trust Codex hooks after Git-hook failure (#955)
SaulMoro Oct 3, 2026
bdeec66
fix(init): trust Codex hooks after Git-hook failure (#955)
SaulMoro Oct 3, 2026
6143e45
test(hooks): align startup fixtures with shared main hooks (#955)
SaulMoro Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/designs/data-directory-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,14 @@ every checkout, so that is where they live now:
├── learnings-wt/ getWorktreeDir → <dataHome>/<dirname>
├── reports-wt/ (the side-branch locks sit beside them)
├── pending-learnings/ pendingLearningsDir → <dataHome>/pending-learnings
├── managed-main-checkout-hooks.json team hooks teamai wrote ungated into the main checkout's Claude Code / Codex
│ settings, shared by every checkout (#955; the built-in hooks stay in HOME)
│ project toolPaths choose the files; Claude uses settings.local.json beside its settings file
│ Codex records event, matcher-group position and complete rendered entry; unique definitions recover moved entries
│ legacy ownership matches event/matcher/command uniquely, ignoring unrecorded timeout/context options
│ pre-#370 Codex ownership is imported from <main>/.teamai/managed-hooks.json before reconcile/removal
└── workspaces/<managedMcpWorkspaceId(root)>/
├── managed-main-checkout-hooks.json bare repositories only: this workspace owns its Claude / Codex team-hook files and trust target
├── managed-mcp.json managedMcpManifestPath, one per checkout; Copilot placement is true for bare, false for keyed, absent when unproven
├── managed-mcp-files.json resolvedMcpFilesPath: project MCP configs teamai may have written a resolved ${VAR} to, and whether
│ the paths earlier teamai.yaml revisions mapped were read; one of those git tracks is marked tracked (#882);
Expand Down
25 changes: 15 additions & 10 deletions docs/usage-guide.md

Large diffs are not rendered by default.

25 changes: 15 additions & 10 deletions docs/usage-guide.zh-CN.md

Large diffs are not rendered by default.

42 changes: 34 additions & 8 deletions skill-data/core/references/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,22 @@ This is the #1 onboarding issue. In order:
```bash
teamai hooks inject
```
4. **Wrong scope?** Project-scope hooks are written to your HOME tool settings
(e.g. `~/.claude/settings.json`), not the project folder — that is intentional.
4. **Wrong scope?** Project-scope built-in hooks are written to your HOME tool
settings (e.g. `~/.claude/settings.json`), not the project folder; the team's own
hooks for Claude Code and Codex go to the main checkout
(`.claude/settings.local.json`, `.codex/hooks.json`). That is intentional.
Existing Claude/Codex main-checkout hook files count as installed targets
even when HOME and current worktree tool roots are missing. Injection and
pull update team hooks and restore HOME built-ins; removal clears managed
main-checkout hooks without recreating HOME roots.
If Git-hook installation fails after writing agent hooks, `hooks inject`,
`init` and self-repo bootstrap still attempt Codex trust. Injection preserves
the installation error without reporting overall success. Init reports the
error and retains exit code 1 while completing local setup, including HTTP
initialization. Bootstrap records the error in the debug log and continues
local setup.
In project scope, `teamai hooks remove` preserves other projects' gated team
hooks in HOME, while removing the shared built-in hooks.
If you initialized project scope but expected machine-wide resources, re-run
with `--scope user`.
5. **Tool has no hook surface** (e.g. Gemini CLI, JoyCode): there is no auto-sync;
Expand Down Expand Up @@ -173,7 +187,7 @@ broken machine):
| Tool | Hooks status | Why |
|-----------------------|---------------------------|---------------------------------------------------------------------|
| Claude Code (`claude`)| Installed | Fully supported — this is the main, working path |
| Codex | Written but **trust-gated** or skipped | Codex gates non-managed hooks behind an explicit trust step; `teamai doctor` prints a reminder to trust them |
| Codex | Installed and trusted | Codex runs only trusted hooks; teamai trusts the ones it writes through `codex app-server`, and `teamai doctor` names any Codex will not run |
| Cursor | Installed | Also runs `~/.claude/settings.json`. That copy exits only when `~/.cursor/hooks.json` or the project `.cursor/hooks.json` contains `--tool cursor` |
| Copilot CLI | Installed in self mode | Also runs a trusted project's `.claude/settings.json`. That copy exits only when `.github/hooks/teamai.json` contains `--tool copilot`. `COPILOT_CLI` alone does not skip |
| CodeBuddy / WorkBuddy | Installed | Claude-format hooks in their own `settings.json` |
Expand All @@ -192,11 +206,23 @@ step — do not assume auto-sync just works.

### Codex

Codex gates non-managed hooks behind an explicit **trust** step. `teamai init` /
`teamai hooks inject` may write the hooks, but Codex won't run them until the user
trusts them (`teamai doctor` prints a reminder when it detects this). Guide the
user to trust the teamai hooks in Codex, then reopen a session. Until then, run
`teamai pull` manually.
Codex runs a non-managed hook only once it is **trusted**. `teamai init`, `pull`
and `teamai hooks inject` trust the hooks they write (and, in a project, the main
checkout, or the current worktree for a bare repository) through `codex app-server`.
Trust written by a session-start pull applies from the next Codex session. `teamai doctor` names any teamai hook Codex will not
run. Then: run `teamai pull`; if `codex` is not on PATH or `codexTrustEnabled: false`
is set in `config.yaml`, guide the user to trust the teamai hooks in Codex `/hooks`,
then reopen a session. A new linked worktree gets the team hooks from its second
Codex session (the first creates its `.codex/`). Member hooks with the same command
are preserved and remain untouched by automatic trust. Codex ownership uses the
recorded event, position and complete entry. A moved entry is recovered only by a
unique full-definition match. Legacy records recover only a unique event, matcher
and command match; `timeout` and `additionalContextLimit` were not recorded.
Pre-#370 project Codex ownership is imported from the main checkout's
`.teamai/managed-hooks.json` before reconciliation or direct removal.
Unrecorded or ambiguous legacy team-hook copies are preserved. Project hook paths follow `toolPaths`;
Claude uses `settings.local.json` beside its configured settings file. A custom
Codex path that Codex does not load is reported as `not loaded` by doctor.

### Cursor

Expand Down
5 changes: 3 additions & 2 deletions skill-data/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ and create-repo URLs, and the per-provider caveats, and points at
install. Let `teamai init` set up every AI tool already installed (omitting
`--agent` gives an interactive picker; select all detected tools). **After init,
report which agents were set up** — in the user's language, which tools now
auto-start TeamAI, and which detected tools were skipped and why (e.g. Codex
trust-gate, CodeBuddy design). Verify the real per-tool result with
auto-start TeamAI, which detected tools were skipped and why (e.g. CodeBuddy
design), and any installed hooks that still need trust (e.g. Codex with
automatic trust disabled or unavailable). Verify the real per-tool result with
`teamai doctor` and `teamai hooks list`.
3. **`teamai init` ends with a pull.** In user scope, and in project scope for each
tool named with `--agent` (or picked in init's tool picker when a person runs it
Expand Down
5 changes: 3 additions & 2 deletions skill-data/setup/references/setup-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,9 @@ login` run in an interactive shell (see Step 3).
Claude Code"). Omitting `--agent` gives an interactive picker — select **every AI
tool already installed** on the machine. Then **report back which agents were set
up**, in the user's language: name the tools that will now auto-start TeamAI, and
any detected tool that was skipped and why (e.g. Codex trust-gate,
CodeBuddy/WorkBuddy by design — see the troubleshooting reference, `"$(teamai skill path core)/references/troubleshooting.md"`).
any detected tool that was skipped and why (e.g. CodeBuddy/WorkBuddy by design),
and any installed hooks that still need trust (e.g. Codex with automatic trust
disabled or unavailable). See the troubleshooting reference, `"$(teamai skill path core)/references/troubleshooting.md"`.

## Step 6 — Verify with doctor

Expand Down
1 change: 1 addition & 0 deletions src/__tests__/anchors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ describe('defaultProjectSlug (#809)', () => {
const checkout = path.join(base, 'bare-layout', wt);
git(bare, 'worktree', 'add', '-q', checkout);
expect((await resolveAnchors(checkout))?.projectAnchor).toBe(bare);
expect((await resolveAnchors(checkout))?.projectAnchorIsBare).toBe(true);
expect(await defaultProjectSlug(checkout)).toBe('bare-layout');
}
const sub = path.join(base, 'bare-layout', 'main', 'pkg');
Expand Down
53 changes: 52 additions & 1 deletion src/__tests__/bootstrap-self.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import YAML from 'yaml';
vi.mock('../providers/index.js', async (importOriginal) => ({
...await importOriginal<typeof import('../providers/index.js')>(),
getProvider: vi.fn(() => ({
isAuthenticated: () => true,
authenticate: async () => 'tester',
parseRepoInput: (remote: string) => ({ httpsUrl: remote }),
})),
}));

import { installFakeCodex, readFakeCodexState } from './helpers/fake-codex.js';
import { bootstrapSelfRepo } from '../bootstrap.js';
import { detectProjectConfig } from '../config.js';
import * as gitHook from '../git-hook.js';
import { log } from '../utils/logger.js';

let tmpDir: string;

Expand All @@ -13,6 +25,7 @@ beforeEach(() => {
});

afterEach(() => {
vi.unstubAllEnvs();
fs.rmSync(tmpDir, { recursive: true, force: true });
});

Expand All @@ -33,6 +46,44 @@ describe('bootstrapSelfRepo', () => {
expect(result).toBe('skip');
});

it.each([
{ silent: true, gitHookFailure: false },
{ silent: true, gitHookFailure: true },
{ silent: false, gitHookFailure: true },
])('trusts self project hooks during bootstrap, silent=$silent, gitHookFailure=$gitHookFailure', async ({ silent, gitHookFailure }) => {
tmpDir = fs.realpathSync.native(tmpDir);
const home = path.join(tmpDir, 'home');
const project = path.join(tmpDir, 'project');
const teamaiDir = path.join(project, '.teamai');
const codexHome = path.join(home, '.codex');
fs.mkdirSync(teamaiDir, { recursive: true });
fs.mkdirSync(codexHome, { recursive: true });
const fakeBin = installFakeCodex();
vi.stubEnv('HOME', home);
vi.stubEnv('PATH', `${fakeBin}${path.delimiter}${process.env.PATH ?? ''}`);
fs.writeFileSync(path.join(teamaiDir, 'teamai.yaml'), YAML.stringify({
team: 'test', mode: 'self', repo: 'https://github.com/acme/app.git', provider: 'github',
toolPaths: { codex: { skills: '.codex/skills', settings: '.codex/hooks.json' } },
}));
const failure = new Error('EACCES: read-only .git/config');
const install = vi.spyOn(gitHook, 'installGitHook');
if (gitHookFailure) install.mockRejectedValueOnce(failure);
const debug = vi.spyOn(log, 'debug').mockImplementation(() => {});
try {
expect(await bootstrapSelfRepo(project, { silent })).toBe('bootstrapped');
if (gitHookFailure) expect(debug).toHaveBeenCalledWith(expect.stringContaining(failure.message));
const state = readFakeCodexState(codexHome);
expect(state.projects[project]).toEqual({ trust_level: 'trusted' });
expect(Object.keys(state.hooksState).length).toBeGreaterThan(0);
expect(Object.keys(state.hooksState).some((key) => key.includes(':session_start:'))).toBe(true);
expect(Object.keys(state.hooksState).every((key) => key.startsWith(path.join(project, '.codex', 'hooks.json')))).toBe(true);
} finally {
install.mockRestore();
debug.mockRestore();
fs.rmSync(fakeBin, { recursive: true, force: true });
}
});

it("returns 'already' when a local config.yaml is already present", async () => {
const teamaiDir = path.join(tmpDir, '.teamai');
fs.mkdirSync(teamaiDir, { recursive: true });
Expand Down
Loading
Loading