Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions packages/coding-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ See [docs/settings.md](docs/settings.md) for all options.

### Project Trust

On interactive startup, pi asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions.
On interactive startup, pi asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` or `.claude/skills` and has no saved decision for the folder or a parent folder in `~/.pi/agent/trust.json`. Trusting a project allows pi to load `.pi/settings.json` and `.pi` resources, install missing project packages, and execute project extensions.

Before the trust decision, pi loads only context files, user/global extensions, and CLI `-e` extensions so they can handle the `project_trust` event. Project-local extensions, project package-managed extensions, and project settings are loaded only after the project is trusted. This split also applies when switching to a session from a different cwd whose trust has not been resolved in the current process.

Expand Down Expand Up @@ -289,7 +289,7 @@ Use this skill when the user asks about X.
2. Then that
```

Place in `~/.pi/agent/skills/`, `~/.agents/skills/`, `.pi/skills/`, or `.agents/skills/` (from `cwd` up through parent directories) or a [pi package](#pi-packages) to share with others. See [docs/skills.md](docs/skills.md).
Place in `~/.pi/agent/skills/`, `~/.pi/skills/`, `~/.agents/skills/`, `~/.claude/skills/`, `.pi/skills/`, `.agents/skills/`, or `.claude/skills/` (the last two from `cwd` up through parent directories) or a [pi package](#pi-packages) to share with others. See [docs/skills.md](docs/skills.md).

### Extensions

Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)

#### project_trust

Fired before step decides whether to trust a project with dynamic configs (`.stepcode` or `.agents/skills`). It runs during startup and when session replacement (for example `/resume`) enters a cwd whose trust has not been resolved in the current process. Only user/global extensions and CLI `-e` extensions participate; project-local extensions are not loaded until after trust is resolved.
Fired before step decides whether to trust a project with dynamic configs (`.stepcode`, `.agents/skills`, or `.claude/skills`). It runs during startup and when session replacement (for example `/resume`) enters a cwd whose trust has not been resolved in the current process. Only user/global extensions and CLI `-e` extensions participate; project-local extensions are not loaded until after trust is resolved.

```typescript
pi.on("project_trust", async (event, ctx) => {
Expand Down
4 changes: 3 additions & 1 deletion packages/coding-agent/docs/sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ const { session } = await createAgentSession({
- Project extensions (`.stepcode/extensions/`)
- Project skills:
- `.stepcode/skills/`
- `.agents/skills/` in `cwd` and ancestor directories (up to git repo root, or filesystem root when not in a repo)
- `.agents/skills/` and `.claude/skills/` in `cwd` and ancestor directories (up to git repo root, or filesystem root when not in a repo)
- Project prompts (`.stepcode/prompts/`)
- Context files (`AGENTS.md` walking up from cwd)
- Session directory naming
Expand All @@ -354,7 +354,9 @@ const { session } = await createAgentSession({
- Global extensions (`extensions/`)
- Global skills:
- `skills/` under `agentDir` (for example `~/.stepcode/agent/skills/`)
- `~/.stepcode/skills/`
- `~/.agents/skills/`
- `~/.claude/skills/`
- Global prompts (`prompts/`)
- Global context file (`AGENTS.md`)
- Settings (`settings.json`)
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Step considers a project to have resources that require trust when it finds any
- `.stepcode/settings.json`
- `.stepcode/extensions`, `.stepcode/skills`, `.stepcode/prompts`, or `.stepcode/themes`
- `.stepcode/SYSTEM.md` or `.stepcode/APPEND_SYSTEM.md`
- project `.agents/skills` in the current directory or an ancestor directory
- project `.agents/skills` or `.claude/skills` in the current directory or an ancestor directory

A bare `.stepcode` directory does not count as a project resource that requires trust.

Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Edit directly or use `/settings` for common options. To save startup model defau

## Project Trust

On interactive startup, step asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.stepcode/agent/trust.json`. Trusting a project allows step to load `.stepcode/settings.json` and `.stepcode` resources, install missing project packages, and execute project extensions.
On interactive startup, step asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` or `.claude/skills` and has no saved decision for the folder or a parent folder in `~/.stepcode/agent/trust.json`. Trusting a project allows step to load `.stepcode/settings.json` and `.stepcode` resources, install missing project packages, and execute project extensions.

Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore those project resources, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run.

Expand Down
9 changes: 6 additions & 3 deletions packages/coding-agent/docs/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,22 @@ Step loads skills from:

- Global:
- `~/.stepcode/agent/skills/`
- `~/.stepcode/skills/`
- `~/.agents/skills/`
- `~/.claude/skills/` (Claude Code's user skill directory)
- Project (only after the project is trusted):
- `.stepcode/skills/`
- `.agents/skills/` in `cwd` and ancestor directories (up to git repo root, or filesystem root when not in a repo)
- `.agents/skills/` and `.claude/skills/` in `cwd` and ancestor directories (up to git repo root, or filesystem root when not in a repo)
- Packages: `skills/` directories or `pi.skills` entries in `package.json`
- Settings: `skills` array with files or directories
- CLI: `--skill <path>` (repeatable, additive even with `--no-skills`)

Discovery rules:
- In `~/.stepcode/agent/skills/` and `.stepcode/skills/`, direct root `.md` files are discovered as individual skills when they have valid skill frontmatter with a non-empty `description`
- In `~/.stepcode/agent/skills/`, `~/.stepcode/skills/`, and `.stepcode/skills/`, direct root `.md` files are discovered as individual skills when they have valid skill frontmatter with a non-empty `description`
- In all skill locations, directories containing `SKILL.md` are discovered recursively
- In `~/.agents/skills/` and project `.agents/skills/`, root `.md` files are ignored, but nested `.md` files in grouping folders are discovered when they declare skill frontmatter
- In `~/.agents/skills/`, `~/.claude/skills/`, and project `.agents/skills/` and `.claude/skills/`, root `.md` files are ignored, but nested `.md` files in grouping folders are discovered when they declare skill frontmatter
- Root Markdown files other than `SKILL.md` that do not look like skills are ignored silently
- A skill reached through several locations (for example `~/.claude/skills/foo` symlinked to `~/.agents/skills/foo`) is loaded once
- Directory symlinks are followed once per scan, so cycles do not cause repeated traversal
- `.gitignore`, `.ignore`, and `.fdignore` rules are applied relative to the directory containing each ignore file

Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ Append to the default prompt without replacing it with `APPEND_SYSTEM.md` in eit

### Project Trust

On interactive startup, step asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.stepcode/agent/trust.json`. Trusting a project allows step to load `.stepcode/settings.json` and `.stepcode` resources, install missing project packages, and execute project extensions.
On interactive startup, step asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` or `.claude/skills` and has no saved decision for the folder or a parent folder in `~/.stepcode/agent/trust.json`. Trusting a project allows step to load `.stepcode/settings.json` and `.stepcode` resources, install missing project packages, and execute project extensions.

Before the trust decision, step loads only context files, user/global extensions, and CLI `-e` extensions so they can handle the `project_trust` event. Project-local extensions, project package-managed extensions, and project settings are loaded only after the project is trusted. This split also applies when switching to a session from a different cwd whose trust has not been resolved in the current process.

Expand Down
81 changes: 54 additions & 27 deletions packages/coding-agent/src/core/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,14 +432,21 @@ function findGitRepoRoot(startDir: string): string | null {
}
}

function collectAncestorAgentsSkillDirs(startDir: string): string[] {
/**
* Directories whose `skills/` subdirectory is auto-discovered in the user's home
* and in cwd and its ancestors. `.claude` keeps skills authored for Claude Code
* usable without duplicating them.
*/
const SHARED_SKILL_DIR_NAMES = [".agents", ".claude"] as const;

function collectAncestorSkillDirs(startDir: string, dirName: string): string[] {
const skillDirs: string[] = [];
const resolvedStartDir = resolve(startDir);
const gitRepoRoot = findGitRepoRoot(resolvedStartDir);

let dir = resolvedStartDir;
while (true) {
skillDirs.push(join(dir, ".agents", "skills"));
skillDirs.push(join(dir, dirName, "skills"));
if (gitRepoRoot && dir === gitRepoRoot) {
break;
}
Expand Down Expand Up @@ -2358,12 +2365,19 @@ export class DefaultPackageManager implements PackageManager {
prompts: join(projectBaseDir, "prompts"),
themes: join(projectBaseDir, "themes"),
};
const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills");
const userSharedSkillDirs = SHARED_SKILL_DIR_NAMES.map((name) => join(getHomeDir(), name, "skills"));
// ~/.stepcode/skills sits beside the agent dir rather than inside it. It is
// user-owned, so when cwd is $HOME it is not also read as a project directory.
const userConfigBaseDir = join(getHomeDir(), this.configDirName);
const userConfigSkillsDir = join(userConfigBaseDir, "skills");
const projectSkillsIsUserConfig = resolve(projectDirs.skills) === resolve(userConfigSkillsDir);
const projectTrusted = this.settingsManager.isProjectTrusted();
const includeSkills = accumulator.resourceTypes.includes("skills");
const projectAgentsSkillDirs =
const projectSharedSkillDirs =
includeSkills && projectTrusted
? collectAncestorAgentsSkillDirs(this.cwd).filter((dir) => resolve(dir) !== resolve(userAgentsSkillsDir))
? SHARED_SKILL_DIR_NAMES.flatMap((name) => collectAncestorSkillDirs(this.cwd, name)).filter(
(dir) => !userSharedSkillDirs.some((userDir) => resolve(dir) === resolve(userDir)),
)
: [];

const addResources = (
Expand Down Expand Up @@ -2391,7 +2405,7 @@ export class DefaultPackageManager implements PackageManager {
);

// Project skills from the product's configuration directory.
if (includeSkills) {
if (includeSkills && !projectSkillsIsUserConfig) {
addResources(
"skills",
collectAutoSkillEntries(projectDirs.skills, "pi"),
Expand All @@ -2402,19 +2416,19 @@ export class DefaultPackageManager implements PackageManager {
}
}

// Project skills from .agents/ (each with its own baseDir)
for (const agentsSkillsDir of projectAgentsSkillDirs) {
const agentsBaseDir = dirname(agentsSkillsDir); // the .agents directory
const agentsMetadata: PathMetadata = {
// Project skills from .agents/ and .claude/ (each with its own baseDir)
for (const sharedSkillsDir of projectSharedSkillDirs) {
const sharedBaseDir = dirname(sharedSkillsDir); // the .agents or .claude directory
const sharedMetadata: PathMetadata = {
...projectMetadata,
baseDir: agentsBaseDir,
baseDir: sharedBaseDir,
};
addResources(
"skills",
collectAutoSkillEntries(agentsSkillsDir, "agents"),
agentsMetadata,
collectAutoSkillEntries(sharedSkillsDir, "agents"),
sharedMetadata,
projectOverrides.skills,
agentsBaseDir,
sharedBaseDir,
);
}

Expand Down Expand Up @@ -2454,19 +2468,32 @@ export class DefaultPackageManager implements PackageManager {
globalBaseDir,
);

// User skills from ~/.agents/ (with its own baseDir)
const userAgentsBaseDir = dirname(userAgentsSkillsDir);
const userAgentsMetadata: PathMetadata = {
...userMetadata,
baseDir: userAgentsBaseDir,
};
addResources(
"skills",
collectAutoSkillEntries(userAgentsSkillsDir, "agents"),
userAgentsMetadata,
userOverrides.skills,
userAgentsBaseDir,
);
// User skills from ~/.stepcode/skills (the config dir, not the agent dir)
if (resolve(userConfigSkillsDir) !== resolve(userDirs.skills)) {
addResources(
"skills",
collectAutoSkillEntries(userConfigSkillsDir, "pi"),
{ ...userMetadata, baseDir: userConfigBaseDir },
userOverrides.skills,
userConfigBaseDir,
);
}

// User skills from ~/.agents/ and ~/.claude/ (each with its own baseDir)
for (const userSharedSkillsDir of userSharedSkillDirs) {
const userSharedBaseDir = dirname(userSharedSkillsDir);
const userSharedMetadata: PathMetadata = {
...userMetadata,
baseDir: userSharedBaseDir,
};
addResources(
"skills",
collectAutoSkillEntries(userSharedSkillsDir, "agents"),
userSharedMetadata,
userOverrides.skills,
userSharedBaseDir,
);
}
}

addResources(
Expand Down
30 changes: 19 additions & 11 deletions packages/coding-agent/src/core/trust-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ const TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [
] as const;

/**
* Entries that double as the user's own global file at ~/<configDir>/<entry>.
* When cwd is $HOME the project path resolves to that same file, so treating it
* Entries that double as the user's own global file or directory at
* ~/<configDir>/<entry> (config.toml, and skills/ which is auto-discovered as
* user skills). When cwd is $HOME the project path resolves to that same entry, so treating it
* as project input would prompt for trust on the user's own configuration - and
* a "do not trust" answer there would be inherited by every project below $HOME.
*/
const USER_GLOBAL_CONFIG_RESOURCES: ReadonlySet<string> = new Set(["config.toml"]);
const USER_GLOBAL_CONFIG_RESOURCES: ReadonlySet<string> = new Set(["config.toml", "skills"]);

/**
* Compare two already-resolved paths for filesystem equality.
Expand Down Expand Up @@ -207,14 +208,16 @@ function withTrustFileLock<T>(path: string, fn: () => T): T {

/**
* Returns true when cwd has project-local resources that must be gated by
* project trust: trust-requiring entries under cwd/.pi, or .agents/skills in
* cwd or one of its ancestors. Returns false when no such project resources
* exist. The user/global ~/.agents/skills directory is always treated as a
* trusted user resource and is ignored here, even when cwd is $HOME.
* project trust: trust-requiring entries under cwd/.pi, or .agents/skills or
* .claude/skills in cwd or one of its ancestors. Returns false when no such
* project resources exist. The user/global ~/.agents/skills and ~/.claude/skills
* directories are always treated as trusted user resources and are ignored
* here, even when cwd is $HOME.
*/
export function hasTrustRequiringProjectResources(cwd: string, configDirName: string = CONFIG_DIR_NAME): boolean {
const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));
const userAgentsSkillsDir = join(homeDir, ".agents", "skills");
const sharedSkillDirNames = [".agents", ".claude"];
const userSharedSkillDirs = sharedSkillDirNames.map((name) => join(homeDir, name, "skills"));
let currentDir = canonicalizePath(resolvePath(cwd));

const resolvedConfigDirName = configDirName.trim() || CONFIG_DIR_NAME;
Expand All @@ -228,9 +231,14 @@ export function hasTrustRequiringProjectResources(cwd: string, configDirName: st
}

while (true) {
const agentsSkillsDir = join(currentDir, ".agents", "skills");
if (!isSamePath(agentsSkillsDir, userAgentsSkillsDir) && existsSync(agentsSkillsDir)) {
return true;
for (const name of sharedSkillDirNames) {
const sharedSkillsDir = join(currentDir, name, "skills");
if (
!userSharedSkillDirs.some((userDir) => isSamePath(sharedSkillsDir, userDir)) &&
existsSync(sharedSkillsDir)
) {
return true;
}
}

const parentDir = dirname(currentDir);
Expand Down
Loading
Loading