NAME
Skilletor - Install skills, agents and rules from remote sources – the Perl port of skilletor
VERSION
version 0.001
SYNOPSIS
use Skilletor;
my $sk = Skilletor->new( home => $ENV{HOME}, project_dir => $project );
# The skill as an include, straight from the source tree or the shared cache:
my $file = $sk->resolve_skill('perl-moo'); # …/cache/79eb247685294166/skills/perl-moo/SKILL.md
my $agent = $sk->resolve_agent('reviewer'); # the project's wins over the personal one
my @rules = $sk->resolve_rules('style'); # both scopes' rules, the user's first
my $item = $sk->resolve_item( skill => 'perl-moo' );
# { type, name, scope, source, version, source_dir, path, dir, file, files,
# description, vars, declaredBy, via?, templated, templates }
for my $rule ( @{ $sk->items( type => 'rule' ) } ) { ... }
# Startup without the network: what the cache holds.
my $offline = Skilletor->new( home => $ENV{HOME}, project_dir => $project, fetch => 'never' );
# Install for Claude Code, as `skilletor sync` does:
use Skilletor::Report qw( report_text exit_code );
my $report = $sk->sync; # the reference's SyncReport
print report_text($report) || 'skilletor: up to date', "\n";
my $due = $sk->check->{changed}; # is a sync due?
my $status = $sk->status;
# The reference's resolveItems, field for field:
my $resolution = $sk->resolve( scope => 'all' );
DESCRIPTION
skilletor installs skills, agents and rules from remote sources (git repositories, https tarballs, local directories) into Claude Code, Codex and Hermes, at user and project scope. Its reference implementation is TypeScript; this distribution is its Perl port, living in the same repository, for Perl programs – an agent such as Langertha::Raider – that must resolve skills exactly as skilletor does, straight from the same config and the same cache, without Node, and sync them into Claude Code's directories as the CLI does.
The port shares everything on disk with the reference: the config files (~/.claude/skilletor.json, <project>/.claude/skilletor.json, skilletor.local.json), the source cache under ~/.claude/skilletor/cache (the same directory names, so one fetch serves both), trust.json, the sync lock, the lock files and the state records check reads, and the bytes either writes. Error messages and warnings are the reference's, word for word; the shared conformance cases (conformance/ in the repository) hold both to that.
The lookups ("resolve_skill" and its siblings, "items") answer "where is skill X" the way a sync would install it, but without installing anything: an item is served from the source tree it lies in – a local directory or the cache dir a git or url source was fetched into. "sync", "check" and "status" are the CLI's commands for the Claude Code target (Skilletor::Engine).
Scopes and precedence
Every scope resolves on its own, as a sync installs it: the user scope from the user config alone, the project scope from the merged project, local and user config (spec §3). Within a scope a type and name is one item at most – the overlap rules (explicit entries over wildcards and bundles, two sources offering one name skip it) see to that. Across the scopes a sync installs an item declared in both twice, once per scope; which one Claude Code then uses depends on the type, and a lookup without a scope follows Claude Code's documented rules (names ignore case, as everywhere in skilletor):
Skills: the personal one wins. "Enterprise over personal, and personal over project": with
deployin both~/.claude/skills/and the project's.claude/skills/,/deployruns the personal one (code.claude.com/docs/en/skills). "resolve_skill" answers the user's.Agents: the project's wins. Claude Code takes managed, then
--agents, then the project's.claude/agents/, then~/.claude/agents/, then plugins (code.claude.com/docs/en/sub-agents). "resolve_agent" answers the project's.Rules: neither wins. Claude Code loads the user's rules before the project's, and neither set overrides the other (code.claude.com/docs/en/memory): both are in effect. "resolve_rules" answers both, the user's first; "resolve_rule" answers one only when one scope declares it – with both it throws Skilletor::Error::Ambiguous rather than hide one.
Pass scope => 'user' or scope => 'project' to look in one scope only; then there is never more than one.
Fetching
"fetch" is the fetch policy: always (default) resolves every source as a sync does – a git fetch, a url download, a failure falling back to the cache with a warning; missing fetches only a source with no usable cache yet; never touches no network and serves what the cache holds, a source that was never fetched being a warning. Every lookup method answers from one resolution of every scope, made at the first call; "refresh" drops it, and so does "sync". The policy holds for "sync" too: with never it syncs from the cache alone.
Templates
The Perl port renders no Nunjucks. An item with a .njk file resolves like any other – its record says templated => 1 and lists the templates – but "resolve_skill" and its siblings refuse it with a Skilletor::Error::Unsupported: an unrendered template is never served as if it were the item. A "sync" never installs one either: it warns and leaves such an item as the reference installed it, and its scope stays open for the reference's next sync ("Templates" in Skilletor::Engine).
Claude Code only
"sync", "check" and "status" serve Claude Code alone. A scope that targets Codex or Hermes – by "targets", or by Codex detected on the machine without "targets" – is refused with an error in the report, and a sync is refused where the reference left Codex or Hermes state behind; nothing is written then ("Claude Code only" in Skilletor::Engine). Unlike the lookups, they need an agent harness, as the reference does.
Modules
Skilletor::Resolver – the resolve (the reference's
resolveItems); Skilletor::Engine – sync, check, status on top of it.Skilletor::Config – load, merge and validate the three config files; edit them.
Skilletor::Catalog, Skilletor::Bundles, Skilletor::Frontmatter – source trees, bundle files and their YAML subset.
Skilletor::Entry – install entries, patterns and bundle entries.
Skilletor::Spec – resolve an
addshorthand to a source.Skilletor::Backend – choose a source's backend (author mode) and build it.
Skilletor::Source::Local, Skilletor::Source::Git, Skilletor::Source::Url – the backends (Skilletor::Role::Source), with Skilletor::Cache, Skilletor::Tar and Skilletor::Gzip.
Skilletor::Render – an item's files in memory; Skilletor::Apply – onto the disk, against the lock; Skilletor::Lock – lock files.
Skilletor::Gitignore, Skilletor::ClaudeSettings, Skilletor::Briefing – the managed blocks, the project's plugin requirement, the briefing check.
Skilletor::State – trust, the sync lock and the records
checkreads.Skilletor::Targets – harness detection, the targets merge, the layout table.
Skilletor::Report – the reports' text and exit codes, as the CLI words them.
Skilletor::JSON, Skilletor::JSString, Skilletor::URL, Skilletor::FSUtil, Skilletor::GitEnv, Skilletor::Process – JavaScript and Node behaviour where it reaches a byte, a name or a message.
CONVENTIONS
Data structures carry the reference's field names (derivedName, declaredBy, trustRequests) so they compare with the TypeScript side directly; methods are snake_case. Paths are character strings, as the reference's are; Perl hands their UTF-8 bytes to the file system. A home directory is always passed in – nothing reads $ENV{HOME} on its own.
home
Required: the home whose .claude/skilletor.json and cache are used.
project_dir
The project; none (or the home dir itself) means the user scope alone.
fetch
always (default), missing or never; see "Fetching".
require_harness
True: the lookups fail as the reference does when no agent harness (Claude Code, Codex) is detected and the user config names no targets. Default false: resolving needs no harness. "sync", "check" and "status" always need one.
state_root
cache_root
codex_home
timeout_ms
lock_timeout_ms
Passed to Skilletor::Resolver; its defaults without them.
is_git_work_tree
git_tracked
git_remote
plugin_root
Passed to Skilletor::Engine, where they are described; the questions to git and the plugin root they answer default to git and CLAUDE_PLUGIN_ROOT.
resolver
The Skilletor::Engine (a Skilletor::Resolver) doing the work.
engine
The same as "resolver".
resolve
my $report = $sk->resolve( scope => 'user' | 'project' | 'all' );
A fresh resolve, the reference's ResolveReport (see Skilletor::Resolver): { scopes => [ { scope, items, sources, warnings, trustRequests } ] }, or { scopes => [], error => $message } on a config error.
resolve_item
my $rec = $sk->resolve_item( skill => 'perl-moo', scope => 'all' );
The record of the item a lookup finds by the type's precedence ("Scopes and precedence"), or undef for a name no scope resolves to (also when the item was declared but its source or the item itself could not be read – the warning in "resolve" says why):
{
type => 'skill', name => 'perl-moo', scope => 'project',
source => 'shared', version => 'git:30993b3',
source_dir => '/home/me/.claude/skilletor/cache/b966aec947fa313b',
path => 'skills/perl-moo', # relative to source_dir, as in the report
dir => '…/skills/perl-moo', # a skill's directory; an agent's or rule's parent
file => '…/skills/perl-moo/SKILL.md', # the main file
files => [ '…/skills/perl-moo/SKILL.md', … ],
description => 'Write Moo classes', # when the frontmatter has one
vars => { … }, declaredBy => [ … ], via => [ … ],
templated => 0, templates => [], # the .njk files, relative
}
A rule both scopes declare throws Skilletor::Error::Ambiguous unless a scope is given. A config error throws Skilletor::Error::Config; an unreadable lock file and a sync lock held too long throw too.
resolve_items
my $recs = $sk->resolve_items( rule => 'style' );
Every record of the type and name, one per scope that declares it, in the type's order: skills user then project, agents project then user, rules user then project. The first is the one Claude Code uses for a skill or an agent; for rules all are in effect.
resolve_skill
my $file = $sk->resolve_skill( 'perl-moo', scope => 'all' );
The absolute path of the skill's SKILL.md in its source tree, or undef; with both scopes declaring the skill, the user's. Throws Skilletor::Error::Unsupported for a templated skill.
resolve_agent
The agent's <name>.md, as "resolve_skill"; with both scopes declaring the agent, the project's.
resolve_rule
The rule's <name>.md, as "resolve_skill" – when one scope declares it. With both, Claude Code loads both: this throws Skilletor::Error::Ambiguous; ask "resolve_rules", or pass a scope.
resolve_rules
my @files = $sk->resolve_rules( 'style', scope => 'all' );
The <name>.md of every scope's rule of the name, the user's first; none when no scope declares it. Throws Skilletor::Error::Unsupported when one of them is templated.
items
my $records = $sk->items( type => 'skill', scope => 'all' );
Every item record ("resolve_item"), skills first, then agents, then rules, each type in its order: one skill per name (the user's winning), one agent per name (the project's winning), and every rule of every scope. type and scope are optional filters.
sync
my $report = $sk->sync( scope => 'all', force => 0 );
Install what the config declares for Claude Code, as skilletor sync does, and drop the resolution the lookups answer from. The reference's SyncReport ("sync" in Skilletor::Engine); a config error, no agent harness, a scope targeting another harness are its error, with nothing touched. Throws when a write fails midway (the lock then owns what landed, marked partial). Skilletor::Report words it.
check
my $check = $sk->check( scope => 'all' );
print "a sync is due\n" if $check->{changed};
The reference's CheckReport ("check" in Skilletor::Engine): whether a sync is due, without one.
status
my $status = $sk->status( scope => 'all' );
The reference's StatusReport ("status" in Skilletor::Engine), offline.
refresh
Drop the resolution the lookup methods answer from; the next call resolves again (and fetches, by "fetch").
SEE ALSO
https://github.com/Getty/skilletor – the reference implementation and its design spec, docs/superpowers/specs/2026-09-22-skilletor-design.md.
SUPPORT
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.