pkgskills
Ship prompts inside a CLI package and install thin, version-stamped stubs where the harness reads them.
A host is a Python package that bundles skills, rules, and subagent
definitions as package data for Claude Code and Codex. pkgskills gives that
package three things:
- a
skillcommand that prints a bundled prompt on demand, so the text an agent reads always comes from the installed version and there is no copy to go stale; - an
installcommand that materializes the files the harness must read off disk, each stamped in its frontmatter with the host andpkgskillsversions and the install mode; - an
install --checkthat tells a current file from a drifted, missing, or foreign one and exits non-zero unless everything isok.
Skills are installed as stubs: the frontmatter the harness needs to know
when to fire, plus an instruction to run <cli> skill <name> and follow the
output.
Rules and agents are installed as copies, because the harness reads their
full text with no model in the loop. Both carry the same stamp and the same
drift check. By default, every supported harness receives each artifact kind:
| Harness | Skills | Rules | Agents |
|---|---|---|---|
| Claude Code | .claude/skills/<name>/SKILL.md |
.claude/rules/<name>.md |
.claude/agents/<name>.md |
| Codex | .agents/skills/<name>/SKILL.md |
Not installed | Not installed |
These paths are relative to $HOME for global mode or the repository root
for local mode. Codex discovers repo skills in .agents/skills from the
working directory up to the repository root, and user skills in
~/.agents/skills. It loads global and local skills with the same name as
separate entries; they are neither merged nor shadowed. Codex custom agents
use TOML rather than these Markdown definitions, and its .rules files are
execution policy rather than prompt rules, so neither kind is installed.
Host.harnesses defaults to (CLAUDE_CODE, CODEX); set it explicitly to
restrict the target tools. An artifact installs only where its kind is
supported, and a declaration no harness supports is rejected.
Reference documents are the fourth thing a host declares and
the one that is never installed at all: they are printed by <cli> doc <name>,
which is how a body defers detail to a sidecar it can no longer reach by path.
Install
uv add pkgskills
Python 3.11 or later. The runtime dependencies are typer and PyYAML.
Declare a host
Put the prompts inside the package (hatchling ships .md files under a
package directory by default) and declare them once:
# yourtool/cli.py
import typer
from pkgskills import Agent, Doc, Host, Rule, Skill, register
HOST = Host(
dist="yourtool", # distribution name, for the version lookup
cli="yourtool", # bare command; local mode prefixes `uv run`
prompts="yourtool.prompts", # package holding the prompt files
artifacts=(
Skill(
name="yourtool",
sources=("skills/add/SKILL.md", "skills/close/SKILL.md"),
),
Rule(name="yourtool", source="rules/yourtool.md", render_cli=True),
Agent(name="yourtool-reviewer", source="agents/reviewer.md"),
),
docs=(Doc(name="add/fields", source="skills/add/references/fields.md"),),
)
app = typer.Typer()
register(app, HOST) # adds skill, install, doc, rule, agent
Each skill source is a spec-conformant skill directory — <name>/SKILL.md,
holding the skill's references/ and scripts/ too — so the prompts/ tree
passes a skills linter as it stands. The skills/ group above is a
convenience, not a requirement: pkgskills reads only the last two components
of the path, so a host that ships nothing but skills can drop it and write
sources=("add/SKILL.md",). A flat skills/add.md still installs and may be
mixed in, but assert_spec_conformant(HOST) reports it, along with anything else
misfiled, naming the rule and the fix — see
docs/source-layout.md.
A skill with one source lifts that file's frontmatter into the stub byte for
byte — including a metadata.version the source declares as the skill's own —
and adds the stamp after it (see
docs/frontmatter.md). Its body is addressed by the
skill's name — <cli> skill use-solo — so the source need not be named
after it. A skill with several sources becomes a dispatcher: each source's
name — its directory, or its stem for a flat file — is a subcommand, and the
stub tells the agent to run <cli> skill <subcommand>. Its stub lists the subcommands
in a table, with each source's description and its metadata.version, or an empty
cell for a source that declares none. The stub's metadata holds only the stamp.
The two namespaces share one argument, so a dispatcher subcommand may not collide
with another skill's name; Host rejects that at construction, as it rejects a
Skill.name outside the spec's grammar.
A body must not point at a file by a path relative to the stub: after
install the stub is alone in its directory and there is nothing there to
read. Ship the file as a Doc and refer to it as {cli} doc <name> — see
Documents.
Set render_cli=True on an artifact or doc whose body uses the {cli}
placeholder; it is rendered as yourtool for a global install and
uv run yourtool for a local one, so printed commands run as written. When
every body a host ships uses the token, set render_cli=True on the Host
instead and leave the declarations alone; an explicit flag on a declaration
still wins, so one body can opt back out.
Register the host under the pkgskills.hosts entry-point group and the
pkgskills script can find it:
[project.entry-points."pkgskills.hosts"]
yourtool = "yourtool.cli:HOST"
The commands a host gains
| Command | Does |
|---|---|
yourtool skill [NAME] [--list] |
Print a skill body, frontmatter stripped. NAME is a skill's name, or a dispatcher's subcommand; it is optional when the host ships exactly one body. |
yourtool doc [NAME] [--list] |
Print a reference document (only when the host ships docs). |
yourtool rule [NAME] [--list] |
Print a rule (only when the host ships rules). |
yourtool agent [NAME] [--list] |
Print an agent definition (only when the host ships agents). |
yourtool install |
Write every artifact for the host's default mode — under ~/.claude/ and ~/.agents/ unless the host restricts its modes. |
yourtool install --local / --global |
Write them under the enclosing repository, or under ~/.claude/ and ~/.agents/. Naming a mode the host does not declare is an error. |
yourtool install --check |
Report ok, drifted, stale, missing, or foreign per file (and unreadable per declared line); exit 1 unless all ok. |
yourtool install --force |
Replace files the host did not generate. |
yourtool permissions [--level L] [--global] [--apply] [--codex] |
Print or apply an automation-level allow-rule profile; --codex prints a Codex rules fragment and refuses --apply (only when the host declares one). |
The enclosing repository is the nearest ancestor of the working directory
holding .git, .claude/, or .codex/, so a local install from a subdirectory
still lands where the harness loads from. Home's own .claude/ and .codex/ do
not count, since that is where a global install lives. ~/.git does, so a
dotfiles repository keeps its root.
Your own commands
A command you write beside these does not inherit that walk. One that reaches
for Path.cwd() writes under whatever subdirectory it ran in, while
install --local on the same CLI writes at the root. pkgskills cannot enforce
this from Host or register, so resolve the root the way the mounted commands
do:
from pkgskills import find_repo_root
@app.command()
def index() -> None:
root = find_repo_root(harnesses=HOST.harnesses)
...
Pass harnesses. Without it only .git marks a root, so in a repository whose
subdirectory holds .claude/ or .codex/, the two halves of the CLI pick
different directories.
The walk always returns a directory, and outside a repository that directory is
rarely the one you want. With no marker at all it returns where it started,
including under a home with a ~/.claude or ~/.codex, which the walk does not
stop at. A command that should not write outside a repository checks for one:
root = find_repo_root(harnesses=HOST.harnesses)
if not any((d / ".git").exists() for d in (root, *root.parents)):
raise SystemExit("run this inside a git repository")
Modes
Global (default) puts one copy under $HOME that serves every repository;
the CLI is on PATH and invoked bare. Local puts the copy under the
repository root and invokes the CLI through uv run. The relative layout is
the same at both bases, so the two collide only where the root is $HOME
itself: from $HOME, or under a home that holds .git. The paths then name one
set of files, which serve every repository, so they count as global. --check
judges each file once, and --local is refused. A host that installs only
locally is exempt, since it has no global copy to overwrite.
A file's mode is the one its location implies. A stub rendered for local mode
and carried to the global path reads as drifted, because the commands inside
it are wrong where it sits. When both a global and a local copy of a skill
exist, Claude Code loads the global one; install and --check say so.
Codex loads both, so its local copy is reported as stale once a global
install resolves.
A host that supports only one
A host whose skills only mean anything inside one repository — they read that
repo's files, or drive its history — has no use for global mode, and a stray
yourtool install would write stubs under $HOME that then shadow the
per-repo ones. Declare the modes it actually supports:
HOST = Host(..., modes=("local",))
The first mode listed is what a flagless install uses and what printed
bodies render {cli} for before anything is installed, so the flag becomes
optional rather than mandatory. Asking for the other mode (--global here) is
an error naming the host's modes, not a silent redirect, and pkgskills.install
refuses it too. The checks that only make sense across two bases — a per-repo
copy gone stale under a global install, a global skill shadowing a local stub
— are skipped, since neither can happen.
This is a per-host constraint. Which mode a particular repository expects is
a separate question, and not one pkgskills answers yet.
Documents
A skill body that says "read references/fields.md before rewriting anything"
works while the body is a file in a skill directory and stops working the
moment it is printed from a package: there is no directory next to the stub,
and the path resolves to nothing. A Doc is that sidecar, declared:
HOST = Host(
...,
docs=(Doc(name="add/fields", source="skills/add/references/fields.md"),),
)
The body then says {cli} doc add/fields, and the document is printed the same
way a skill body is — frontmatter stripped, {cli} resolved for the mode the
install actually resolves to. Because it loads only when a step asks for it,
the detail stays out of context until it is needed.
A doc is not an artifact. It is never written, stamped, checked, or removed;
install, install --check, and pkgskills check do not know it exists, and
the only place it has to ship is the wheel. Names may contain / so a host can
namespace its documents by the skill that owns them; that is a convention, not
something pkgskills interprets. A doc's name and its source are independent,
which is what lets the file live inside that skill's own directory —
skills/add/references/fields.md — so the shipped tree matches the spec's
skill layout while the body still writes {cli} doc add/fields. Since nothing else ever reads a doc's source, a
missing one is rejected when the Host is constructed rather than when a model
runs the command.
A host that would rather keep a print command of its own can: build the body
with pkgskills.render_prompt(text, host.invocation(mode)) and get mode from
pkgskills.printing_mode(host, root), which is what skill and doc use — the
installed mode when there is one, the host's default before the first install.
Resolving it any other way prints commands that do not run.
The stamp and the check
Every generated file carries five keys under its frontmatter's metadata, and a
rule or agent whose source has no frontmatter gains a block to hold them:
---
metadata:
pkgskills-host: "yourtool"
pkgskills-host-version: "1.4.0"
pkgskills-version: "0.1.0"
pkgskills-mode: "local"
pkgskills-note: "generated; install overwrites edits"
---
Those keys are the only place the releases are recorded. They join whatever
metadata the source declares, after its own keys, so metadata.version stays
free to mean the skill's own version. The pkgskills- prefix is reserved: a source
declaring a key in it fails assert_spec_conformant and cannot be rendered.
install --check re-renders each artifact and compares, with the stamp's two
version values masked, so upgrading either package never flags a file whose
content did not change. What sits at the path decides the verdict:
| At the path | Status | Reason reported |
|---|---|---|
| nothing | missing |
not installed |
| our render, any versions | ok |
|
| our stamp, different content | drifted |
content differs, or rendered for the other mode |
the HTML comment an earlier pkgskills wrote |
drifted |
the stamp now lives in frontmatter, or rendered for the other mode |
a stamp the host declared in legacy_stamps |
drifted |
stamped before the host adopted pkgskills, or rendered for the other mode |
| a local copy a global install superseded | stale |
still loaded; remove it |
| a file with no stamp | foreign |
hand-written or from an older release |
| another package's stamp | foreign |
generated by that package |
| a symlink, dangling or live | foreign |
a symlink, not a plain file |
| a directory | foreign |
a directory, not a file |
| unreadable or not UTF-8 | foreign |
unreadable |
install refuses to touch a foreign file without --force, and it checks
every target before writing the first one, so a refused rule never leaves a
half-installed skill behind. With --force, a symlink is replaced by a plain
file rather than written through. A fresh global install removes per-repo
copies the host generated earlier; a local install never deletes the global
copy that serves other repositories.
Migrating from your own stamps
A host that stamped its generated files before adopting pkgskills leaves those
files on every machine that installed an earlier release, and they read
foreign until someone passes --force. Declare the old stamp and a bare
install upgrades them:
Host(
...,
legacy_stamps=(
re.compile(r"<!-- yourtool \S+ \((?P<mode>global|local)\); do not edit -->"),
),
)
Each pattern has to match a whole line and capture the recorded mode as a named
group mode, so a stamp that recorded no mode cannot be declared. Construction
rejects a pattern with no such group, and one that matches a stamp pkgskills
wrote itself: any line of the current map, or the HTML comment of earlier releases,
which is recognized without a declaration.
A matching file counts as generated everywhere. It reads drifted, a bare
install overwrites it, edits included, and it is removed as a stale local copy
or at a previous name. That is how a current stamp is treated, so declare a
pattern only for a stamp that also told readers not to edit the file, and match
it exactly, with any version token as \S+. A loose pattern hands install a
hand-written file. Drop a pattern once no supported upgrade starts from a release
that wrote it; pkgskills cannot tell when that is.
Renaming an artifact
A host that renames a rule, skill, or agent leaves the old file behind at
every consumer — still auto-loading, still stamped as the host's. Declare the
old name and install cleans it up:
Rule(name="yourtool", source="rules/yourtool.md", previous_names=("plan-files",))
At each previous name, the file that would sit there in the install mode is
removed only if it carries this host's stamp — the same test a stale local
copy gets. install --check reports such a leftover as stale and says
remove:. A file at an old name without the stamp is somebody else's: it is
left alone, mentioned in a note, and never gates. A single-source skill's stub
lifts the source's name, so renaming one means renaming the source's
frontmatter as well; a dispatcher renames from the declaration alone.
stale is the asymmetric case. Claude Code auto-loads rules and agents from
the global and the local location at once, so a per-repo copy left behind
after a switch to global is an extra file live in context whatever its content
says. The verdict therefore outranks both ok and drifted: the check gates on
it and says remove:, not repair: — rewriting the file is not the fix, and a
reinstall would only recreate it. Claude Code skills are exempt, because a
global skill shadows the local stub rather than loading alongside it. Codex
skills load from both bases, so its leftover local stubs are stale too;
which kinds load from both bases is declared on each Harness. A global
copy during a local install is never flagged: it is shared infrastructure
serving every other repository.
One line in a file the host does not own
Some generated files need one line in a file that belongs to the repository:
the motivating case is a merge=union attribute for a generated index, so it
resolves on merge instead of conflicting. Declare the line and install
keeps it there:
HOST = Host(
...,
lines=(Line(path=".gitattributes", key="docs/README.md", value="merge=union"),),
)
key is the line's first field, the token that identifies it; value is the
rest. install appends the line when no line names the key, and rewrites a
line that names the key with a different value only under --force — the
file is repo content a user may have set deliberately, so a plain install
reports the difference and leaves it. Other lines are never touched. The line
goes in the repository whatever the install mode, since it has no meaning
under $HOME.
install --check shows each line under its path with a blank mode column:
| Found | Status |
|---|---|
| a line naming the key with the value | ok |
| a line naming the key with another value | drifted (the check says what the file gives) |
| no line naming the key, or no file | missing |
| a file that cannot be read | unreadable |
All four but ok gate. unreadable is its own status rather than folding into
foreign because --force cannot fix it: an installer that cannot read the
file refuses to rewrite it, since every edit here is an append or a one-line
amendment of text it has read, and a status must not imply a remedy the tool
will not perform. A line is not stamped and not an artifact; it renders from
nothing, and a stamp has no place inside another tool's file.
Wiring pre-commit hooks
A host that ships a validate or index command usually wants it to run as
a git hook. pkgskills.precommit does the wiring; the host declares only the
hooks and calls it from the two host hooks:
from pkgskills import Hook, InstallReport
from pkgskills import precommit
HOOKS = (
Hook("yourtool-validate", args=("validate",), files=r"^docs/plans/.*\.md$"),
Hook(
"yourtool-index",
stage="post-merge",
args=("index", "."),
always_run=True,
pass_filenames=False,
),
)
pass_filenames follows pre-commit's default (true), so a file-scoped hook
like validate gets the matched paths on its command line; a whole-repo hook
that takes none, like the post-merge index above, turns it off explicitly.
def after_install(report: InstallReport) -> None:
precommit.wire(report, HOOKS)
HOST = Host(
...,
after_install=after_install,
extra_checks=lambda host, root, mode: precommit.checks(host, root, mode, HOOKS),
)
wire appends a repo: local block naming the hooks that are missing from
.pre-commit-config.yaml (creating the file when absent, and seeding repos:
in one that is empty or comments only), and runs
pre-commit install --hook-type <stage> for each distinct stage. Each hook's
entry is the host's invocation for the install mode plus the hook's args,
so it can only be mode-correct; name and files are single-quoted, so a
regex or a display name may carry : or #. Three rules keep it from
clobbering repo content:
- A hook already present is keyed on its
- id:line, so a customized entry counts as present and is not duplicated, and an id that is a prefix of another (x-indexnext tox-index-all) is not mistaken for it. - An entry naming the other mode is resynced only on a genuine switch —
when
InstallReport.previous, the mode resolved before the write, differs from the mode being installed. A same-mode refresh leaves it: a host's own dev repo keepsuv run yourtoolwhile using a global stub, since the host is a local dependency there. An entry naming neither invocation is always left. - An unreadable config is never rewritten;
HookReport.unreadablesays so.
wire returns a HookReport: whether the config was created, which hook ids
were added or resynced, and per stage what registration did — activated,
already_active, skipped (with activate=False), no_git_repo,
hookspath_blocked (git's core.hooksPath makes pre-commit install refuse),
or unavailable. pre-commit itself is invoked bare in global mode and
through the host's local_prefix in local mode; pass precommit= to
override. precommit.add_dependency(root) runs uv add --dev pre-commit, and
is a separate call because it edits pyproject.toml, which a default install
should not do unasked.
checks returns one row per hook — active, unregistered, missing (no
entry in the config), no_git_repo, or blocked — none of which gate, since
registration is per-clone state a fresh clone legitimately lacks. Each row's
note names the command that fixes it. Registration is judged by the hook
script pre-commit writes into the effective hooks directory, which honors
core.hooksPath and linked worktrees, so a hook live at a non-default path is
still found.
Hooks for host-specific work
Host.after_install receives an InstallReport once every artifact is on
disk: the mode, the root, the paths written, removed, and shadowed, the mode
that was installed before (previous), the user's force consent, what
happened to each declared line, and the files removed or left at previous
names. Use it for follow-up the library cannot know about; shell out through
pkgskills.run(root, argv), which pins the call to root and strips GIT_DIR
and its siblings, so an inherited location variable cannot aim a commit at
another repository.
Host.extra_checks is the read side of the same idea. Called with (host, root, mode) during install --check, it returns ExtraCheck(label, status, gates, note) rows for per-clone state pkgskills cannot see. They print in the
same table, and only the rows that say they gate fold into the exit code — a
hook that is not registered in this clone deserves a line without calling a
correct install broken.
Hosts that need extra flags keep their own install command and call
pkgskills.install(host, root, mode, force=...) and
pkgskills.check(host, root, mode) directly.
Automation levels
A host whose skills tell the model to run commands can declare, per level, the Bash allow-rules that level adds:
HOST = Host(
...,
permissions={
Level.assist: ("Bash(git add:*)", "Bash(git commit:*)", "Bash(uv run:*)"),
Level.confirm: ("Bash(git push:*)",),
Level.full: ("Bash(gh pr merge:*)",),
},
)
The levels are an escalating, superset ladder named by supervision posture —
none (grant nothing), assist (local and reversible), confirm (adds the
publishing step), full (adds the irreversible one) — with 0–3 as
aliases. yourtool permissions prints the cumulative union for a level; the
grant for calling the CLI itself is derived from the host's invocation, so it
appears for a global profile and collapses into Bash(uv run:*) for a local
one.
--apply merges into .claude/settings.local.json (or ~/.claude/settings.json
with --global). The merge is additive and never downgrades: a rule already on
deny or ask stays there and is reported as skipped, a rule already allowed
is a no-op, and an apply with nothing to add leaves the file untouched. So the
command is safe to run blind.
Codex sandbox
In Codex's workspace-write sandbox, the workspace and /tmp are writable,
with protected directories inside those roots:
- Every command that writes git state (
git commit,git branch,git worktree add,git merge,git tag, and similar operations) fails because.gitis read-only, whether it is a directory or a pointer file. In a linked worktree, the resolved git directory is protected too. install --localfails when the.agents/directory already exists: Codex keeps it read-only recursively even inside the workspace..codex/and.aws/directories are protected the same way.- A global
installwrites under~/.claude/and~/.agents/, outside the workspace, so it also needs permission to run outside the sandbox. install --checkandskill/docprinting are read-only and run sandboxed.
Approve the escalation when prompted with the on-request approval policy,
grant the needed command prefixes with yourtool permissions --codex, or run
the command outside Codex. --codex prints a paste-ready execution-policy
fragment for the requested level. Its target is
.codex/rules/yourtool.rules locally (loaded only for a trusted project), or
~/.codex/rules/default.rules with --global.
An allow rule runs matching commands outside the sandbox without
prompting. Codex also accepts prompt and forbidden; the most restrictive
matching decision wins. Translation covers Bash(<words>:*) and
Bash(<words> *) with a trailing *, splitting the words the way a shell
would into a command prefix. An exact Bash(<words>) is not translated:
Codex matches by prefix only, so the rule would grant every extension of the
command as well. A rule with shell syntax (&&, |, ;, a redirection, a
$ expansion, a leading ~) is not translated either: Codex matches the
parsed argv, which never contains those tokens. Exact rules, shell syntax,
other wildcards, and non-Bash tools appear as # not translated: comments. Review the fragment and paste it into the target file;
--codex --apply is refused, and pkgskills never writes Codex .rules files.
The pkgskills script
pkgskills hosts # every host registered in this environment
pkgskills check # run each host's drift check from the current repository
Testing a host
pkgskills.testing.sandbox(tmp_path, monkeypatch) pins $HOME and the working
directory to fresh directories, so a suite never touches the developer's real
~/.claude or ~/.agents.
pkgskills.testing.wheel_files(project_root, out_dir) builds a wheel
in-process and lists its contents, which is the only way to prove the prompts
ship: an editable install resolves package data straight to the checkout.
pkgskills.testing.assert_spec_conformant(host) checks every skill the host
ships against the
Agent Skills specification:
that each source is stored as <name>/SKILL.md, that its frontmatter carries a
name matching the directory and satisfying the spec's grammar, that a
description is present, that metadata is the map of strings the spec calls
for (an unquoted version: 1.0 is a float, not a string), and that no field
runs past its limit. Every
violation is reported at once, each naming the rule it breaks and the fix — see
docs/source-layout.md.
That includes the layout, so a host that adopts the helper as written uses
<name>/SKILL.md for every source, a dispatcher's included. A host that keeps
flat sources on purpose filters the report instead
(why):
violations = [v for v in HOST.check_spec() if v.rule != "entry-file"]
assert not violations, "\n".join(map(str, violations))
pkgskills.testing.assert_prompt_commands(host, app) closes the loop the whole
pattern exists for. pkgskills renders {cli}, but nothing otherwise checks
that what follows it is a command the host actually has, and prose about a CLI
goes stale. The helper scans every skill body, doc, rule, and agent for {cli} ... mentions, resolves each command path against the typer app, and resolves
the argument to skill, doc, rule, and agent against the host's own
declarations — so a renamed doc or a dropped subcommand fails the suite with the
source and line of every mention that no longer reaches anything:
def test_prompts_are_well_formed() -> None:
assert_spec_conformant(HOST)
assert_prompt_commands(HOST, app)
A mention counts when it is written as code — inside backticks or a fenced
block. Prose that names the bare placeholder ("{cli} is substituted per
mode") is talking about the token, so the words after it are not read as a
command path. pkgskills.testing.prompt_commands(host) returns the same
mentions as PromptCommand records (source, line, command path, declared
argument) for a suite that wants to assert something else about them.
Development
uv sync --all-groups
uv run pytest
uv run ruff check . && uv run ruff format --check .
uv run pyrefly check
tests/fixtures/ holds three throwaway hosts that the suite drives end to
end: one with every artifact kind, one with a single skill body, and one with
several single-source skills that is also local-only, ships reference
documents inside their own skill directories, and declares render_cli once on
the host. All three store their skills the way the spec does, so the tree a
host author copies is conformant as it stands; brokenhost is the fourth, and
holds the counterexamples the negative tests need.
Metadata
Release files for pkgskills 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pkgskills-0.6.0.tar.gz | 85.2 kB | With provenance | Details for pkgskills-0.6.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pkgskills-0.6.0-py3-none-any.whl | Python 3 | none | any | Details for pkgskills-0.6.0-py3-none-any.whl |
Total release size: 161.9 kB
Release files / pkgskills-0.6.0.tar.gz
| Download URL | pkgskills-0.6.0.tar.gz |
|---|---|
| Size | 85.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cf59a2f570162ec7712f0c33f67b16e2d29d29b3898e886865ca865acd95fc37
|
|
BLAKE2b-256 checksum How to use checksums |
e0b2843799e1d3ce32fbb13000ea8e9039db1c0fb0ed47289fb40b9fa3a4a4e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logRelease files / pkgskills-0.6.0-py3-none-any.whl
| Download URL | pkgskills-0.6.0-py3-none-any.whl |
|---|---|
| Size | 76.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c7fbf3139d294d37172f8b6978748428707f912cbeb00053d23e76ed1f34b1ec
|
|
BLAKE2b-256 checksum How to use checksums |
455704e40ea74ae47d4b8178d94e982520436831a36805e121b6b552cb52732c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logSecurity for pkgskills 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pkgskills-0.6.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.