HCLI plugin bundle specification
Goal
HCLI must be able to install IDA plugins and their Python dependencies on machines that cannot reach the network. A connected machine builds a plugin bundle archive. An offline machine can copy that archive locally and use normal plugin manager commands against it.
The plugin bundle is repository-level. It may contain multiple plugins, multiple versions, and the shared wheelhouses (directories of pre-downloaded .whl files) needed to install those plugins. It is not a per-plugin sandbox, and it does not change HCLI's current dependency installation model: Python dependencies are installed into the active IDA Python environment.
Supported user flows
Create a plugin bundle on a connected machine targeting the current machine's platform and Python:
hcli plugin bundle create \
--path malware-vm-tools-2026-04.hcli-plugin-bundle.zip \
--platform current --python current \
oplog==0.1.3 \
hint-calls==0.1.3
Both --platform and --python are always required. The special value current resolves to the current machine's platform or Python version. The special value all resolves to all supported platforms (linux, windows, macos-arm64, macos-intel) or all supported Python versions (3.10–3.14). Otherwise, pass a specific name like linux or version like 3.12.
Create a plugin bundle for all platforms at specific Python versions:
hcli plugin bundle create \
--path bundle.zip \
--platform all --python 3.12 --python 3.13 \
oplog==0.1.3 \
hint-calls==0.1.3
Create a plugin bundle for specific platforms and Python versions:
hcli plugin bundle create \
--path malware-vm-tools-2026-04.hcli-plugin-bundle.zip \
--platform linux --platform windows --platform macos-arm64 \
--python 3.12 --python 3.13 \
oplog==0.1.3 \
hint-calls==0.1.3
--platform and --python are both repeatable. HCLI builds the cross product of all specified platforms and Python versions. Platform accepts short aliases: linux, windows, macos-arm64, macos-intel, win, etc. Duplicate targets from overlapping aliases (e.g. --platform all --platform linux) are deduplicated.
Create a plugin bundle that includes local/private plugin archives:
hcli plugin bundle create \
--path internal-review.hcli-plugin-bundle.zip \
--platform current --python current \
--repo /path/to/internal-plugin-repository.json \
./dist/internal-plugin-1.2.0.zip \
private-helper==2.0.0
Inspect or search a plugin bundle without manual extraction:
hcli plugin --repo ./malware-vm-tools-2026-04.hcli-plugin-bundle.zip search
hcli plugin --repo ./malware-vm-tools-2026-04.hcli-plugin-bundle.zip search hints
Install from a plugin bundle on an offline machine:
hcli plugin --repo ./malware-vm-tools-2026-04.hcli-plugin-bundle.zip install hint-calls
Upgrade from a plugin bundle:
hcli plugin --repo ./malware-vm-tools-2026-04.hcli-plugin-bundle.zip upgrade hint-calls
In plugin bundle mode, upgrade means "upgrade to the newest compatible version available in this plugin bundle." It does not mean latest upstream.
Advanced usage
Use a corporate pip index instead of the default pip configuration:
hcli plugin --pip-index-url https://pypi.example.corp/simple install ipyida
Use an explicit local wheel source:
hcli plugin --pip-find-links /mnt/wheelhouse --offline install ipyida
Command-line interface
The plugin command group gains these options:
hcli plugin [--repo REPO] [--pip-index-url URL] [--pip-extra-index-url URL] [--pip-find-links PATH_OR_URL] [--offline] COMMAND ...
--repo accepts the existing repository forms plus a local plugin bundle archive. A plugin bundle archive is a ZIP file containing top-level plugin-bundle.json.
--pip-index-url is passed to pip as --index-url. It may point to PyPI, Artifactory, or any other compatible package index.
--pip-extra-index-url is repeatable and passed to pip as --extra-index-url.
--pip-find-links is repeatable and passed to pip as --find-links.
--offline forces pip dependency resolution to use only local sources. It passes --no-index and must be used with either a plugin bundle wheelhouse or at least one --pip-find-links value.
Pip source precedence is:
- Explicit pip CLI options.
- Plugin bundle wheelhouse selected from the active plugin bundle.
- Existing configured/default online pip behavior.
If explicit pip CLI options are provided while using a plugin bundle repository, the explicit options control dependency installation. The plugin bundle still controls plugin discovery and plugin archive bytes.
Plugin bundle archive format
A plugin bundle archive is a ZIP file with this layout:
plugin-bundle.json
plugins/
<archive-name>.zip
dependencies/
python/
<target-id>/
<wheel files>.whl
The plugins/ directory contains unmodified IDA plugin ZIP archives. HCLI discovers bundled plugins by walking plugins/ and reading ida-plugin.json from each archive.
Each dependencies/python/<target-id>/ directory is a flat wheelhouse for one target tuple. Wheels keep their original filenames. Sdists are rejected by default during bundle creation.
Each target entry in the manifest is a snapshot of the exact compatibility parameters used to fetch wheels for that target. This allows audit and install to verify wheel compatibility without re-deriving the parameters.
plugin-bundle.json is UTF-8 JSON. Version 1 has this shape:
{
"version": 1,
"kind": "hcli-plugin-bundle",
"builtAt": "2026-04-28T16:00:00Z",
"createdBy": {
"tool": "hcli",
"version": "0.0.0"
},
"targetPlatformTags": [
{
"id": "linux-x86_64-cp312",
"idaPlatform": "linux-x86_64",
"pythonVersion": "3.12",
"implementation": "cp",
"abis": ["cp312", "abi3", "none"],
"pipPlatformTags": [
"manylinux_2_28_x86_64",
"manylinux_2_17_x86_64",
"manylinux2014_x86_64"
],
"wheelhouse": "dependencies/python/linux-x86_64-cp312"
}
]
}
Target matching
On install or upgrade, HCLI detects the active IDA platform and active IDA Python interpreter. For IDA 9.0+, the first supported Python line is Python 3.10+.
HCLI selects a wheelhouse whose target tuple matches:
- HCLI/IDA platform, such as
windows-x86_64ormacos-aarch64 - Python major/minor version
- CPython implementation and ABI expectations
If no target tuple matches, HCLI fails before changing plugins or installing dependencies.
The bundle acts as a plugin repository during install. Install uses the resolver of bundle creation. HCLI selects the newest bundled version that supports the platform, the IDA version, and IDA's Python version (from requiresPython), and whose required plugin dependencies the bundle can also satisfy for the current installation. Thus a bundle that contains different versions for different platforms or Python versions installs the version that was bundled for the matching cell. Plugin dependencies come from the bundle too, also when the bundle is a configured plugin repository. When a required plugin dependency cannot be resolved, the install fails before it changes plugins or installs Python dependencies.
A plugin bundle may contain target tuples for operating systems and Python versions other than the machine that created it. Cross-target creation is supported when every dependency can be obtained as a compatible wheel for the requested target. If a required dependency is available only as an sdist, or only as a wheel with a platform baseline newer than the target, bundle creation or audit must fail for that target unless the user supplies a matching prebuilt wheel.
IDA version compatibility during bundle creation
Bundle creation does not check IDA version compatibility. The builder machine may run a different IDA version than the target machines, and plugin idaVersions metadata is not a useful filtering axis during packaging. Plugins are bundled as-is; IDA version compatibility is checked only at install time on the target machine. This avoids adding IDA version as yet another dimension to the target matrix (alongside platform and Python version) and keeps the bundle creation model simple. If users report problems with this approach, IDA version filtering can be revisited.
Dependency installation behavior
When installing a plugin with pythonDependencies, HCLI installs dependencies with pip before extracting the plugin. When a plugin bundle is in use, HCLI extracts the matching wheelhouse to a temporary directory and adds it as a --find-links source. By default pip can still reach online indexes, so a bundle with an incomplete wheelhouse will fall back to downloading missing wheels. When the user passes --offline, HCLI adds --no-index so pip resolves only from local sources:
python -m pip install \
--isolated \
--disable-pip-version-check \
--no-cache-dir \
[--no-index] # only when --offline is passed \
--find-links <temporary-wheelhouse> \
<all dependency specs>
HCLI includes dependency specs from already installed HCLI-managed plugins plus the plugin being installed or upgraded. Already installed packages may satisfy requirements if pip considers them compatible. HCLI does not use --ignore-installed by default.
No hash pinning is required. The user trusts the plugin bundle author.
HCLI does not uninstall Python dependencies when plugins are uninstalled. Orphaned dependencies are left in the Python environment, matching current behavior.
Bundle creation behavior
hcli plugin bundle create accepts repository plugin references, local plugin directories, and local plugin ZIP paths as positional arguments. --path selects the output archive path.
A repository plugin reference can be a bare name (oplog), an exact pin (oplog==0.1.3), or a range (oplog>=0.1, oplog~=0.1), with an optional @host suffix. A repo/ prefix, such as internal/oplog, selects the plugin that the configured repository internal lists as oplog. HCLI then resolves the spec as oplog@host, with the host of that plugin, against all configured repositories. Bundle creation fails when the named repository does not list the name, or lists it from more than one host. A dependency on oplog without @host is satisfied by the version that the spec selects, as for any other plugin. When two specs with different prefixes select plugins with different hosts, bundle creation fails with a resolution error. A prefix cannot be combined with --repo, because --repo already selects the only repository that HCLI searches. A plugin with the same name and host in more than one configured repository is one plugin, with the versions of all those repositories.
Each target cell (one platform and one Python version) is a separate resolution. A plugin version is compatible with a cell when it supports the cell's platform and its requiresPython allows the cell's Python version. HCLI tests requiresPython against major.minor.0 of the cell, so a specifier such as >=3.12.1 excludes the whole 3.12 cell. A compatible version is viable for a cell when every required plugin dependency has a viable version for that cell. For each cell, HCLI selects the newest viable version that matches each reference, in command-line order. Thus a bare name gives the newest version whose dependencies also install on each target, and one bundle can contain different versions of a plugin for different platforms or Python versions. The archive filenames then get a platform suffix, for example oplog-0.2.0-linux-x86_64.zip. When two different archives of one version would get the same platform suffix, their filenames get target IDs instead, for example oplog-0.2.0-linux-x86_64-cp311.zip. To get the same version on all platforms, use an exact pin. HCLI writes each resolution of a reference that is not an exact pin to stderr, for example resolved oplog: 0.2.0 (linux-x86_64), 0.1.3 (windows-x86_64), so you can see which versions went into the bundle. Each distinct archive has its own entry in this output. A platform name in this output stands for all cells of that platform; a target ID such as linux-x86_64-cp312 names one cell.
Targeting uses --platform and --python, both required and repeatable. Each accepts current (auto-detect this machine), all (all supported values), or a specific value. --platform accepts the canonical IDA platform name (e.g. linux-x86_64) or short aliases (linux, windows, macos-arm64, macos-intel). --python accepts a major.minor version string (e.g. 3.12). HCLI builds the cross product of all resolved platforms and Python versions. Duplicate targets are deduplicated.
The supported Python versions for --python all are maintained as a hardcoded constant (SUPPORTED_PYTHON_VERSIONS): 3.10, 3.11, 3.12, 3.13, 3.14. This is updated when new Python versions release (annually in October). This matches the approach used by cibuildwheel and other ecosystem tools — no package provides this list dynamically.
A legacy --target flag (e.g. --target linux-x86_64-cp312) is accepted for scripting but hidden from help. It cannot be combined with --platform or --python.
Bundle creation resolves all selected plugin archives, reads their ida-plugin.json metadata, collects the pythonDependencies (including PEP 723 inline dependencies and component dependencies) of the plugins resolved for each target cell, and materializes a flat wheelhouse per target cell from that cell's list only. A plugin version that a cell does not use does not add wheels to that cell's wheelhouse. A single online Linux builder can create wheelhouses for all target platforms when all dependencies publish compatible wheels.
When a bundled plugin declares dependencies (plugin-to-plugin), those dependent plugins are fetched from the repository and included in the bundle, along with their own Python dependencies and their own plugin dependencies. The dependencies of each component of a suite count as dependencies of the suite. Dependencies resolve for each target cell, from the plugin versions selected for that cell. An unpinned dependency resolves to the newest viable version for the cell. When two plugins pin different versions of one dependency, or a pin disagrees with a version already selected for an unpinned dependency, HCLI goes back and tries older versions of the plugins that led to the conflict. The first plugin reference on the command line keeps the highest priority. A dependency that only one cell's plugin versions declare is included for that cell only. HCLI writes each included dependency and its versions to stderr.
A required dependency that cannot be resolved for a cell causes bundle creation to fail, because the plugin that declares it cannot install on that cell. The error names the target ID of the cell, the chain of plugins that led to the dependency, and why each candidate version was rejected, for example cannot resolve plugin-a for windows-x86_64-cp312: plugin-a 1.0.0 needs plugin-b: plugin-b 1.0.0 does not support windows-x86_64. A dependency name without an @host suffix that matches plugins from more than one host stops bundle creation, and the error lists the qualified names to use. HCLI checks the dependencies of every version that can install on the cell for this, required and optional, before it selects versions, so the result does not depend on the order of the specs. A local plugin directory or ZIP whose requiresPython excludes a cell also causes bundle creation to fail, because a local archive has only one version. The error names the plugin, its requiresPython, and the target ID. A local plugin also fails bundle creation when its archive is not valid, for example when its entry point file is missing, or when a dependency pins a version that the local plugin does not have. The error names the cause. An optional dependency that cannot be resolved, or whose own required dependencies cannot be resolved, is left out of the cell. HCLI writes a warning to stderr with the chain, the cells, and the reason, for example warning: skipped optional dependency plugin-a 1.0.0 -> extras (windows-x86_64): extras 1.0.0 does not support windows-x86_64. An optional dependency is also left out of a cell, with the same warning, when pip cannot download the Python dependencies of that dependency or of the plugins that it requires, for example warning: skipped optional dependency plugin-a 1.0.0 -> extras (linux-x86_64): no matching wheel for extras-pkg>=2. The plugins that only that dependency reaches are left out too. Dependency cycles resolve: when two plugins require each other, the bundle contains both. The search stops with an error after 10,000 steps.
The preferred wheelhouse materialization path is one pip download invocation per target. For cross-target downloads HCLI must pass all compatibility options together:
python -m pip download \
--only-binary=:all: \
--implementation cp \
--python-version <major.minor> \
--abi <cp-abi> --abi abi3 --abi none \
--platform <platform-tag> [--platform <platform-tag> ...] \
--dest <wheelhouse> \
-r <requirements-or-lock>
--platform is repeatable. Linux targets should list every manylinux tag HCLI accepts, for example manylinux_2_28_x86_64, manylinux_2_17_x86_64, and manylinux2014_x86_64. macOS x86_64 targets should include the supported deployment baseline. Optional targets such as Windows ARM64 or musllinux should be explicit presets rather than inferred from x86_64 targets. Over-listing compatible tags is safer than under-listing them.
uv pip compile --universal may be used to produce one cross-platform lock or constraints file before materializing target wheelhouses. uv pip install --python-platform ... --python ... --target ... is also valid for producing an unpacked per-target site-packages tree; because the version 1 plugin bundle format stores wheel files, pip download remains the direct path for wheelhouse plugin bundles unless HCLI later adds an unpacked dependency tree format.
By default, bundle creation rejects sdists because offline installation from sdists requires build tools and build dependencies on the target. Missing wheels are handled by failing the affected target, by using a matching builder to produce wheels, or by supplying prebuilt wheels with a local --find-links source. Linux wheels can often be built from one Linux host inside a manylinux container. Windows and macOS sdist-only dependencies require native runners or prebuilt vendor wheels.
If selected plugins have incompatible Python dependency constraints, bundle creation fails.
Additive plugin bundles
Plugin bundles are additive. An offline VM may use one plugin bundle today and a second plugin bundle later. HCLI does not require a single authoritative plugin bundle for the machine.
When a plugin bundle install fails, errors should distinguish where possible: whether the plugin is absent from the bundle, whether it exists but is incompatible with the current IDA version or platform, whether no wheelhouse matches the active IDA Python, whether a dependency wheel is missing from the selected wheelhouse, or whether dependency constraints conflict with already installed packages.
Private plugin metadata
The repository field in ida-plugin.json must be a valid GitHub URL (https://github.com/org/project). This applies to both public and private plugins. Private plugins should use a GitHub repository URL — either a public repo or a private one. If no real repository exists, a placeholder GitHub URL is acceptable (e.g. https://github.com/internal/placeholder) because the public plugin indexer only fetches URLs for plugins it ingests, and offline bundle plugins are never indexed.
The validation is intentionally not relaxed to support arbitrary hosting providers (GitLab, Gitea, etc.) until there are concrete user requirements. Keeping the GitHub constraint preserves the field's usefulness as a consistent, predictable link format across all plugins.
Plugin spec disambiguation
When the plugin repository contains multiple entries with the same plugin name from different hosts (e.g. forks), bundle create requires the @host suffix to disambiguate:
hcli plugin bundle create \
--path bundle.zip \
--platform all --python all \
"efiXplorer==6.2.0@https://github.com/rehints/efixplorer" \
"idalib-rust-bindings==0.9.0@https://github.com/idalib-rs/idalib"
The @host is parsed from the spec before resolution. Unambiguous plugins do not require it.
Known limitations observed during all-plugin bundle testing
Bundle creation pools the pythonDependencies of every plugin resolved for a target cell into a single pip download invocation for that cell. When that invocation fails, HCLI downloads the Python dependencies of the plugins that the specs require, then adds each optional dependency in a separate invocation and leaves out the ones that fail. A single unresolvable Python dependency (nonexistent version, missing wheel for the target) of a required plugin fails the entire wheelhouse for that target.
Plugins observed with broken or unavailable dependencies at the time of testing (2026-05-04):
Frida_Tools,FridaTools: requirepyside6>=6.10.2(no such version on PyPI)idapcode: requirespypcode~=3.3.3(no such version on PyPI; available versions max at 1.1.2)IDAssist: requirespysqlite3(no binary wheels for macOS x86_64 or cp310)ida-cyberchef: requiresSTPyV8(no binary wheels for cp310 targets)fwhunt-ida: requiresfwhunt-scan~=2.3.5which depends onuefi-firmware>=1.10(no wheels for macOS aarch64)
Additionally, codeload.github.com URLs produce archives with non-deterministic hashes, causing SHA256 verification failures for plugins that use them as download URLs (e.g. xray). This is a plugin repository data issue, not a bundle code issue.
Python 3.14 targets often fail because many packages have not yet published cp314 wheels (as of 2026-05-04). The --python all flag includes 3.14 by default; use explicit --python 3.10 --python 3.11 --python 3.12 --python 3.13 to avoid these failures until ecosystem support improves.
A future improvement could add a --skip-on-error flag to bundle creation so that individual plugin dependency failures are reported as warnings rather than aborting the entire build.
Out of scope
The first version does not address per-plugin dependency sandboxes, dependency removal on uninstall, reproducible builds, or wheel redistribution and license compliance. Plugin bundle signatures and external checksum files are also deferred, as is storing corporate pip index settings in ida-config.json.
See also
- Plugin packaging and format — the individual plugin archive format that bundles wrap
- Plugin repository architecture — the online repository that bundles replace for offline use
- CI/CD Integration — building bundles in automated pipelines