feat: Read personal skills from .agents/skills (#10437)

- Issue: https://github.com/agentskills/agentskills/issues/15
- Follow-up to https://github.com/openai/codex/pull/10317 (for team/repo
skills)
- This change now also loads personal/user skills from
`$HOME/.agents/skills` (or `~/.agents/skills`) in addition to loading
from `.agents/skills` inside of git repos.
- The location of `.system` skills remains unchanged.
- Keeping backwards compatibility with `~/.codex/skills` for now until
we fully deprecate.

With skills in both personal folders:
<img width="831" height="421" alt="image"
src="https://github.com/user-attachments/assets/ad8ac918-bfe6-4a2d-8a8e-d608c9d3d701"
/>

We load from both places:
<img width="607" height="236" alt="image"
src="https://github.com/user-attachments/assets/480f4db0-ae64-4dc1-bdf5-c5de98c16f5c"
/>
This commit is contained in:
Gav Verma 2026-02-02 16:49:23 -08:00 committed by GitHub
parent fb2df99cf1
commit e24058b7a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 86 additions and 8 deletions

1
codex-rs/Cargo.lock generated
View file

@ -1422,6 +1422,7 @@ dependencies = [
"core-foundation 0.9.4",
"core_test_support",
"ctor 0.6.3",
"dirs",
"dunce",
"encoding_rs",
"env-flags",

View file

@ -45,6 +45,7 @@ codex-utils-pty = { workspace = true }
codex-utils-readiness = { workspace = true }
codex-utils-string = { workspace = true }
codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" }
dirs = { workspace = true }
dunce = { workspace = true }
encoding_rs = { workspace = true }
env-flags = { workspace = true }

View file

@ -13,6 +13,7 @@ use crate::skills::model::SkillToolDependency;
use crate::skills::system::system_cache_root_dir;
use codex_app_server_protocol::ConfigLayerSource;
use codex_protocol::protocol::SkillScope;
use dirs::home_dir;
use dunce::canonicalize as canonicalize_path;
use serde::Deserialize;
use std::collections::HashSet;
@ -164,7 +165,10 @@ where
outcome
}
fn skill_roots_from_layer_stack_inner(config_layer_stack: &ConfigLayerStack) -> Vec<SkillRoot> {
fn skill_roots_from_layer_stack_inner(
config_layer_stack: &ConfigLayerStack,
home_dir: Option<&Path>,
) -> Vec<SkillRoot> {
let mut roots = Vec::new();
for layer in
@ -182,12 +186,21 @@ fn skill_roots_from_layer_stack_inner(config_layer_stack: &ConfigLayerStack) ->
});
}
ConfigLayerSource::User { .. } => {
// `$CODEX_HOME/skills` (user-installed skills).
// Deprecated user skills location (`$CODEX_HOME/skills`), kept for backward
// compatibility.
roots.push(SkillRoot {
path: config_folder.as_path().join(SKILLS_DIR_NAME),
scope: SkillScope::User,
});
// `$HOME/.agents/skills` (user-installed skills).
if let Some(home_dir) = home_dir {
roots.push(SkillRoot {
path: home_dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME),
scope: SkillScope::User,
});
}
// Embedded system skills are cached under `$CODEX_HOME/skills/.system` and are a
// special case (not a config layer).
roots.push(SkillRoot {
@ -220,15 +233,16 @@ fn skill_roots(config: &Config) -> Vec<SkillRoot> {
#[cfg(test)]
pub(crate) fn skill_roots_from_layer_stack(
config_layer_stack: &ConfigLayerStack,
home_dir: Option<&Path>,
) -> Vec<SkillRoot> {
skill_roots_from_layer_stack_inner(config_layer_stack)
skill_roots_from_layer_stack_inner(config_layer_stack, home_dir)
}
pub(crate) fn skill_roots_from_layer_stack_with_agents(
config_layer_stack: &ConfigLayerStack,
cwd: &Path,
) -> Vec<SkillRoot> {
let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack);
let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack, home_dir().as_deref());
roots.extend(repo_agents_skill_roots(config_layer_stack, cwd));
dedupe_skill_roots_by_path(&mut roots);
roots
@ -829,7 +843,8 @@ mod tests {
let tmp = tempfile::tempdir()?;
let system_folder = tmp.path().join("etc/codex");
let user_folder = tmp.path().join("home/codex");
let home_folder = tmp.path().join("home");
let user_folder = home_folder.join("codex");
fs::create_dir_all(&system_folder)?;
fs::create_dir_all(&user_folder)?;
@ -853,7 +868,7 @@ mod tests {
ConfigRequirementsToml::default(),
)?;
let got = skill_roots_from_layer_stack(&stack)
let got = skill_roots_from_layer_stack(&stack, Some(&home_folder))
.into_iter()
.map(|root| (root.scope, root.path))
.collect::<Vec<_>>();
@ -862,6 +877,10 @@ mod tests {
got,
vec![
(SkillScope::User, user_folder.join("skills")),
(
SkillScope::User,
home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME)
),
(
SkillScope::System,
user_folder.join("skills").join(".system")
@ -877,7 +896,8 @@ mod tests {
fn skill_roots_from_layer_stack_includes_disabled_project_layers() -> anyhow::Result<()> {
let tmp = tempfile::tempdir()?;
let user_folder = tmp.path().join("home/codex");
let home_folder = tmp.path().join("home");
let user_folder = home_folder.join("codex");
fs::create_dir_all(&user_folder)?;
let project_root = tmp.path().join("repo");
@ -906,7 +926,7 @@ mod tests {
ConfigRequirementsToml::default(),
)?;
let got = skill_roots_from_layer_stack(&stack)
let got = skill_roots_from_layer_stack(&stack, Some(&home_folder))
.into_iter()
.map(|root| (root.scope, root.path))
.collect::<Vec<_>>();
@ -916,6 +936,10 @@ mod tests {
vec![
(SkillScope::Repo, dot_codex.join("skills")),
(SkillScope::User, user_folder.join("skills")),
(
SkillScope::User,
home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME)
),
(
SkillScope::System,
user_folder.join("skills").join(".system")
@ -926,6 +950,55 @@ mod tests {
Ok(())
}
#[test]
fn loads_skills_from_home_agents_dir_for_user_scope() -> anyhow::Result<()> {
let tmp = tempfile::tempdir()?;
let home_folder = tmp.path().join("home");
let user_folder = home_folder.join("codex");
fs::create_dir_all(&user_folder)?;
let user_file = AbsolutePathBuf::from_absolute_path(user_folder.join("config.toml"))?;
let layers = vec![ConfigLayerEntry::new(
ConfigLayerSource::User { file: user_file },
TomlValue::Table(toml::map::Map::new()),
)];
let stack = ConfigLayerStack::new(
layers,
ConfigRequirements::default(),
ConfigRequirementsToml::default(),
)?;
let skill_path = write_skill_at(
&home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME),
"agents-home",
"agents-home-skill",
"from home agents",
);
let outcome =
load_skills_from_roots(skill_roots_from_layer_stack(&stack, Some(&home_folder)));
assert!(
outcome.errors.is_empty(),
"unexpected errors: {:?}",
outcome.errors
);
assert_eq!(
outcome.skills,
vec![SkillMetadata {
name: "agents-home-skill".to_string(),
description: "from home agents".to_string(),
short_description: None,
interface: None,
dependencies: None,
path: normalized(&skill_path),
scope: SkillScope::User,
}]
);
Ok(())
}
fn write_skill(codex_home: &TempDir, dir: &str, name: &str, description: &str) -> PathBuf {
write_skill_at(&codex_home.path().join("skills"), dir, name, description)
}
@ -2136,6 +2209,9 @@ interface:
.map(|root| root.scope)
.collect();
let mut expected = vec![SkillScope::User, SkillScope::System];
if home_dir().is_some() {
expected.insert(1, SkillScope::User);
}
if cfg!(unix) {
expected.push(SkillScope::Admin);
}