Skip to content

apm install can drop executable bits for skill scripts in archive/reflink paths #1563

@ic4-y

Description

@ic4-y

Summary

apm install can materialize executable skill scripts as plain non-executable files on some install paths.

This matters for packages that ship runnable skill scripts, e.g. a skill containing scripts/do-driver tracked in git as 100755. The generated skill file is present after install, but invoking it directly fails because the executable bit was not preserved.

Expected behavior

Executable mode metadata is preserved when APM materializes package files.

For a source file tracked or archived as executable:

test -x <generated-skill-path>/scripts/do-driver

should pass after:

apm install -t <target>

Observed behavior

In a downstream consumer install, the workflow driver file was present but not executable.

The affected package source has the file tracked executable in git:

100755 .apm/skills/do/scripts/do-driver

Likely affected paths

I found two places in apm-cli 0.16.0 that appear to copy file contents without preserving mode metadata.

1. Artifactory ZIP extraction

apm_cli/deps/download_strategies.py manually streams ZIP entries to disk:

with zf.open(member) as src, open(dest, "wb") as dst:
    dst.write(src.read())

That writes bytes but does not apply the ZIP entry's Unix mode from ZipInfo.external_attr.

There is already a precedent in apm_cli/deps/registry/extractor.py, where registry ZIP extraction reads the high 16 bits of external_attr and applies mode bits:

unix_mode = (info.external_attr >> 16) & 0xFFFF
...
if unix_mode:
    os.chmod(target, unix_mode & 0o755)

The Artifactory archive extractor should probably mirror that behavior.

2. Reflink copy fast path

apm_cli/utils/file_ops.py has a reflink-backed copy helper:

if clone_file(src, dst):
    return dst

The fallback path uses shutil.copy2(...), which preserves metadata, but the successful reflink fast path returns before copystat/mode preservation.

Suggested fix:

if clone_file(src, dst):
    shutil.copystat(src, dst, follow_symlinks=follow_symlinks)
    return dst

Why this is not just a package issue

The source package has the executable bit. Normal git checkout/copy paths preserve it. The failure appears when APM materializes package files through an archive/proxy or reflink path that writes/clones bytes without copying stat metadata.

Suggested regression tests

Add tests that materialize a package or copied tree containing a 100755 script and assert the installed/generated file remains executable.

Useful cases:

  1. Artifactory/ZIP extraction:

    • Create a ZIP entry with Unix mode 0o100755 in external_attr.
    • Extract through the Artifactory archive path.
    • Assert the extracted file is executable.
  2. Reflink copy helper:

    • Mock or exercise a successful clone_file(src, dst) path.
    • Source file mode is executable.
    • Assert destination mode preserves executable bits.
  3. End-to-end install canary if feasible:

    • Package with a skill script tracked executable.
    • apm install -t claude or another target.
    • Assert generated skills/<name>/scripts/<script> is executable.

Environment

  • apm-cli version: 0.16.0
  • Latest PyPI release checked: 0.16.0
  • Latest GitHub release checked: v0.16.0
  • Upstream main was ahead of v0.16.0 at time of investigation, but I did not find an existing issue for executable-bit preservation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions