# macos-initial-access

Provide macOS guidance for initial access using app bundles, installer packages, and disk images, including current Gatekeeper behavior and pkgbuild and hdiutil examples.

- **Kind:** skill
- **Source:** https://github.com/SpecterOps/skills
- **Page:** https://forefy.com/skills/c5fd7c31-3750-4704-b6c1-c1aa9b9c3eff
- **API (JSON + files):** https://forefy.com/api/asr/c5fd7c31-3750-4704-b6c1-c1aa9b9c3eff

---

## SKILL.md

---
name: macos-initial-access
description: Provide macOS guidance for initial access using app bundles, installer packages, and disk images, including current Gatekeeper behavior and pkgbuild and hdiutil examples.
metadata:
  author: "Outflank"
---

# macOS Initial Access

## Overview

- Some format-specific commands will only run on macOS.
- Clearly label any behavior that depends on signing, quarantine, or management policy.
- You aren't limited to the formats described in this skill.
- Keep commands copyable, explain the expected directory layout in one sentence.

## Trust controls

Explain these in one short paragraph unless the user asks for detail:

- Code signing: Binds developer identity and integrity metadata to code; signatures also carry entitlements. Sign every nested executable, framework, and helper before signing the outer bundle.
- Hardened Runtime: Adds runtime restrictions against injection, process tampering, unsigned executable memory, DYLD manipulation, and untrusted libraries. Exceptions require specific entitlements. It is required for notarization.
- Notarization: Apple's automated malware and signing check for Developer ID-signed software. Successful submissions receive a ticket that Gatekeeper can retrieve or that can be stapled to apps, flat packages, and DMGs.
- Gatekeeper: Evaluates downloaded or quarantined software using signature, notarization, provenance, integrity, and known-malware checks, then asks for user approval when appropriate.

## Format routing

Identify the format or formats the user asks about, then read only the matching reference files. Do not preload unrelated format references. If the user asks only about trust controls, read no format reference. If the user requests a comparison of all options, read all three.

- App bundles (`.app`): Applications containing an `Info.plist` and a Mach-O executable. See [references/app-bundles.md](references/app-bundles.md).
- Installer packages (`.pkg`): Component or product packages containing payload files and optional `preinstall` or `postinstall` scripts, with system or current-user installation domains. See [references/installer-packages.md](references/installer-packages.md).
- Disk images (`.dmg`): Delivery containers commonly used to distribute app bundles. See [references/disk-images.md](references/disk-images.md).

## agents

```

```

## agents/openai.yaml

```yaml
interface:
  display_name: "macOS Initial Access"
  short_description: "Create macOS initial-access payloads"
  icon_small: ./assets/icon.svg
  icon_large: ./assets/icon.png
  brand_color: '#00B36B'
  default_prompt: "Use $macos-initial-access to create an app bundle, installer package, or disk image for macOS."

policy:
  allow_implicit_invocation: true
```

## assets

```

```

## assets/icon.png

```

```

## assets/icon.svg

```

```

## references

```

```

## references/app-bundles.md

# App Bundles

Use this pattern when the user asks for a macOS .app, app bundle, or Application.

- Make the Mach-O executable and ensure `Info.plist` names it correctly.
- Do not say that every unsigned app always prompts. Gatekeeper behavior depends on quarantine, provenance, policy, signature, and notarization state.
- Changing any bundle resource after signing invalidates the signature.

Describe an app as a directory with this minimum shape:

```text
Example.app/
└── Contents/
    ├── Info.plist
    ├── MacOS/
    │   └── Example        # executable Mach-O named by CFBundleExecutable
    └── Resources/         # optional
        └── AppIcon.icns   # optional; named by CFBundleIconFile
```

Here is an example Info.plist:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>en</string>
    <key>CFBundleDisplayName</key>
    <string>Example</string>
    <key>CFBundleExecutable</key>
    <string>Example</string>
    <key>CFBundleIconFile</key>
    <string>AppIcon.icns</string>
    <key>CFBundleIdentifier</key>
    <string>com.example.Example</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>Example</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundleVersion</key>
    <string>1</string>
</dict>
</plist>
```

The icon is optional. If the user has not mentioned an icon, ask whether they
want one. If they do not, omit `CFBundleIconFile`, `AppIcon.icns`, and the
`Resources` directory when it has no other contents.

## Converting an image to an icon (ICNS) file

Use the bundled converter on macOS to turn user-provided artwork into the ten
standard PNG renditions and package them as an ICNS file. A Python 3 script uses
the built-in `sips` command for image processing and writes the ICNS container
directly. It accepts any image format that the local `sips` installation can read;
PNG, JPEG, and ICNS are common inputs. Run `sips --formats` to see the formats
supported on the current host.

From the directory containing this skill's `SKILL.md`, run:

```bash
mkdir -p Example.app/Contents/Resources
./scripts/image-to-icns.py /path/to/image.png \
  Example.app/Contents/Resources/AppIcon.icns
```

The optional second argument is the output path; without it, the script writes
an `.icns` file beside the input image. It refuses to overwrite an existing
output. Add the icon before signing the app; replacing it later invalidates the
bundle signature and requires signing the app again.

Verify the result and keep the file name aligned with `CFBundleIconFile`:

```bash
file Example.app/Contents/Resources/AppIcon.icns
sips -g format Example.app/Contents/Resources/AppIcon.icns
```

## references/disk-images.md

# Disk Images

Treat a DMG as a delivery container, commonly holding an app bundle, package, documentation, or a shortcut to `/Applications`.

```sh
mkdir -p dmg-root
ditto Example.app dmg-root/Example.app
hdiutil create \
  -srcFolder dmg-root \
  -volname "Example" \
  -format UDZO \
  -ov \
  -o Example.dmg
```

## Layout and validation

`-srcFolder` places the contents of the source directory at the image root. Stage the app inside a separate directory so the DMG contains `Example.app`, rather than placing the app's `Contents` directory at the root.

```text
dmg-root/
└── Example.app/
```

Validate nested code before creating the image, then verify the completed DMG:

```sh
codesign --verify --deep --strict --verbose=2 dmg-root/Example.app

hdiutil verify Example.dmg
hdiutil imageinfo Example.dmg
shasum -a 256 Example.dmg
```

`hdiutil verify` validates the disk-image checksum; it does not validate the signature of code inside the image.

The `-ov` option overwrites an existing output image. Omit it when existing artifacts must be preserved.

## Troubleshooting

If `hdiutil` reports `Device not configured`, the current sandbox, container, remote session, or CI runner may not have access to macOS's DiskImages service. Retry from an authorized macOS execution context; this error does not necessarily indicate that the source app bundle is malformed.

## references/installer-packages.md

# Installer Packages

Use this pattern by default for installer packages unless the user explicitly asks for different installation or launch behavior:

- Require no administrator authorization.
- Carry an app inside its scripts archive.
- Copy the app into `~/Applications`.
- Launch the copied app during installation.
- Restrict installation to the current user's home directory.

Put an executable `preinstall` or `postinstall` shell script beside the app in the scripts directory. The examples below use `preinstall`; when `postinstall` timing is required, rename the script and update the matching verification commands without removing the launch step.

## Directory layout

The scripts directory contains the installer script and the complete app bundle:

```text
scripts/
├── preinstall
└── Example.app/
    └── Contents/
        ├── Info.plist
        ├── MacOS/
        │   └── Example
        └── Resources/
```

The app is stored beside `preinstall` so the script can copy it from PackageKit's temporary scripts directory.

## Installer script

Create `scripts/preinstall` with mode `755`:

```sh
#!/bin/sh

SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
DESTINATION_DIR="${HOME}/Applications"
DESTINATION_APP="${DESTINATION_DIR}/Example.app"

/bin/mkdir -p "${DESTINATION_DIR}" || exit 1
/bin/rm -rf "${DESTINATION_APP}" || exit 1
/usr/bin/ditto "${SCRIPT_DIR}/Example.app" "${DESTINATION_APP}" || exit 1
/usr/bin/open -n "${DESTINATION_APP}" >/dev/null 2>&1

exit 0
```

Behavior:

- `SCRIPT_DIR` resolves the temporary directory from which PackageKit extracted the scripts archive.
- `mkdir` creates the current user's Applications directory when necessary.
- `rm -rf` replaces only the fixed `~/Applications/Example.app` destination. Keep this path fixed and never derive it from untrusted input.
- `ditto` preserves the app-bundle structure and relevant metadata.
- `open -n` always asks Launch Services to start a new instance of the copied app.
- A directory-creation, removal, or copy failure aborts installation.
- A launch-request failure does not fail installation because the script intentionally ends with `exit 0`.
- A successful `open` return means Launch Services accepted the request; it does not prove the process remained running.

Make the script executable and validate its syntax:

```sh
chmod 755 scripts/preinstall
sh -n scripts/preinstall
```

To use `postinstall` instead, rename the file and keep the same copy-and-launch sequence:

```sh
mv scripts/preinstall scripts/postinstall
chmod 755 scripts/postinstall
sh -n scripts/postinstall
```

## Distribution.xml

Wrap the component package in a product archive whose Distribution permits only current-user-home installation. For an ARM64-only app:

```xml
<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="2">
    <title>Example</title>

    <options customize="never"
             require-scripts="false"
             hostArchitectures="arm64"/>

    <domains enable_anywhere="false"
             enable_currentUserHome="true"
             enable_localSystem="false"/>

    <choices-outline>
        <line choice="default"/>
    </choices-outline>

    <choice id="default" title="Example" visible="false">
        <pkg-ref id="com.example.example.arm64"/>
    </choice>

    <pkg-ref id="com.example.example.arm64"
             version="1.0"
             onConclusion="none">Example.component.pkg</pkg-ref>
</installer-gui-script>
```

Important details:

- `enable_currentUserHome="true"` and `enable_localSystem="false"` restrict installation to the current user's home directory.
- A current-user-home installation runs its component script as the current user, requires no administrator authorization, and cannot write outside that user's home directory.
- `hostArchitectures="arm64"` prevents installation on incompatible Intel Macs and prevents ARM64 package scripts from being evaluated under Rosetta.
- `require-scripts` refers to JavaScript expressions in the Distribution XML. It does not disable `preinstall` or `postinstall` shell scripts.
- `minSpecVersion="2"` is appropriate for modern macOS Distribution definitions.
- `onConclusion="none"` does not request a logout or restart.
- Give the component package and app stable identifiers so later versions can be distinguished in receipts and logs.

## Build the component package

Verify the source app before packaging:

```sh
file scripts/Example.app/Contents/MacOS/Example
codesign --verify --deep --strict --verbose=2 scripts/Example.app
```

Build a script-only component:

```sh
pkgbuild \
  --nopayload \
  --scripts scripts \
  --identifier com.example.example.arm64 \
  --version 1.0 \
  Example.component.pkg
```

The app is archived under `Scripts/`. The installer script is responsible for replacing the destination, copying the app, and requesting its launch.

## Build the product package

```sh
productbuild \
  --distribution Distribution.xml \
  --package-path . \
  Example.pkg
```

Distribute the resulting product package, not the raw component package. The product Distribution is what restricts installation to the current-user domain.

## Verification

Confirm that the final product permits only a current-user installation:

```sh
installer -dominfo -pkg Example.pkg
```

Expected output:

```text
CurrentUserHomeDirectory
```

Expand the component package without installing it:

```sh
pkgutil --expand-full Example.component.pkg ExpandedComponent
```

The expanded component should have this shape:

```text
ExpandedComponent/
├── PackageInfo
└── Scripts/
    ├── preinstall
    └── Example.app/
```

Confirm that `PackageInfo` contains a `preinstall` entry and inspect the archived script:

```sh
sed -n '1,220p' ExpandedComponent/PackageInfo
sed -n '1,160p' ExpandedComponent/Scripts/preinstall
sh -n ExpandedComponent/Scripts/preinstall
```

Verify that the archived script still includes the launch request:

```sh
grep -F '/usr/bin/open -n "${DESTINATION_APP}"' \
  ExpandedComponent/Scripts/preinstall
```

Verify the embedded app:

```sh
codesign \
  --verify \
  --deep \
  --strict \
  --verbose=2 \
  ExpandedComponent/Scripts/Example.app
```

Inspect signing status and record a hash:

```sh
pkgutil --check-signature Example.pkg
shasum -a 256 Example.pkg
```

Do not execute the package or app merely to validate its archive structure.

## Troubleshooting

### Installer requests administrator authorization

Confirm that the final product, rather than the raw component package, is being opened. Then check its permitted domain:

```sh
installer -dominfo -pkg Example.pkg
```

The output should contain only `CurrentUserHomeDirectory`. Also confirm that the Distribution sets `enable_currentUserHome="true"` and both other domains to `false`.

### Installer reports that installation failed

Inspect `/var/log/install.log` for the package identifier and script name. A message such as:

```text
An error occurred while running scripts from the package
```

normally means `preinstall` or `postinstall` returned nonzero. Check that:

- The script begins with `#!/bin/sh`.
- The script has mode `755`.
- The referenced source app exists beside the script in the scripts archive.
- Required directory, removal, and copy operations intentionally return nonzero on failure.
- `set -e` is not causing an unintended early exit.

### App is missing from `~/Applications`

Check that:

- `HOME` identifies the expected current user's home directory.
- `DESTINATION_APP` is exactly `${HOME}/Applications/Example.app`.
- The archived app name matches the name passed to `ditto`.
- The copy command is reached before the script exits.
- `/var/log/install.log` does not report a `ditto` failure.

### App does not launch or remain running

`open -n` only submits a Launch Services request. Check:

- The app's `CFBundleExecutable` value.
- Execute permission on the Mach-O executable.
- CPU architecture compatibility.
- Code-signature validity.
- Crash reports and application logs.
- Gatekeeper, quarantine, TCC, management policy, and endpoint controls.

If launch failure should also fail installation, make the request explicit:

```sh
/usr/bin/open -n "${DESTINATION_APP}" >/dev/null 2>&1 || exit 1
```

## scripts

```

```

## scripts/image-to-icns.py

```python
#!/usr/bin/env python3
"""Create a complete macOS ICNS file from an image that sips can read."""

from __future__ import annotations

import argparse
import os
import shutil
import struct
import subprocess
import sys
import tempfile
from pathlib import Path


RENDITIONS = (
    (b"icp4", 16, "icon_16x16.png"),
    (b"ic11", 32, "icon_16x16@2x.png"),
    (b"icp5", 32, "icon_32x32.png"),
    (b"ic12", 64, "icon_32x32@2x.png"),
    (b"ic07", 128, "icon_128x128.png"),
    (b"ic13", 256, "icon_128x128@2x.png"),
    (b"ic08", 256, "icon_256x256.png"),
    (b"ic14", 512, "icon_256x256@2x.png"),
    (b"ic09", 512, "icon_512x512.png"),
    (b"ic10", 1024, "icon_512x512@2x.png"),
)


class ConversionError(Exception):
    pass


def run_sips(sips: str, *arguments: object, capture: bool = False) -> str:
    result = subprocess.run(
        [sips, *(str(argument) for argument in arguments)],
        check=False,
        stdout=subprocess.PIPE if capture else subprocess.DEVNULL,
        stderr=subprocess.PIPE,
        text=True,
    )
    if result.returncode != 0:
        detail = result.stderr.strip()
        raise ConversionError(detail or "sips failed")
    return result.stdout or ""


def image_size(sips: str, image: Path) -> tuple[int, int]:
    properties = run_sips(
        sips, "-g", "pixelWidth", "-g", "pixelHeight", image, capture=True
    )
    values = {}
    for line in properties.splitlines():
        key, separator, value = line.strip().partition(":")
        if separator:
            values[key] = value.strip()
    try:
        width = int(values["pixelWidth"])
        height = int(values["pixelHeight"])
    except (KeyError, ValueError) as error:
        raise ConversionError("input has no valid pixel dimensions") from error
    if width < 1 or height < 1:
        raise ConversionError("input has no valid pixel dimensions")
    return width, height


def make_master(sips: str, source: Path, destination: Path, work: Path) -> None:
    width, height = image_size(sips, source)
    if width > height:
        run_sips(
            sips,
            "--setProperty",
            "format",
            "png",
            "--resampleHeight",
            1024,
            source,
            "--out",
            work,
        )
        run_sips(sips, "--cropToHeightWidth", 1024, 1024, work, "--out", destination)
    elif height > width:
        run_sips(
            sips,
            "--setProperty",
            "format",
            "png",
            "--resampleWidth",
            1024,
            source,
            "--out",
            work,
        )
        run_sips(sips, "--cropToHeightWidth", 1024, 1024, work, "--out", destination)
    else:
        run_sips(
            sips,
            "--setProperty",
            "format",
            "png",
            "--resampleHeightWidth",
            1024,
            1024,
            source,
            "--out",
            destination,
        )


def make_renditions(sips: str, master: Path, iconset: Path) -> None:
    iconset.mkdir()
    for _, size, name in RENDITIONS:
        run_sips(
            sips,
            "--resampleHeightWidth",
            size,
            size,
            master,
            "--out",
            iconset / name,
        )


def build_icns(iconset: Path) -> bytes:
    chunks = []
    for kind, _, name in RENDITIONS:
        data = (iconset / name).read_bytes()
        if not data.startswith(b"\x89PNG\r\n\x1a\n"):
            raise ConversionError(f"generated rendition is not PNG: {name}")
        chunks.append(kind + struct.pack(">I", len(data) + 8) + data)
    body = b"".join(chunks)
    return b"icns" + struct.pack(">I", len(body) + 8) + body


def write_new_file(path: Path, data: bytes) -> None:
    try:
        descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
    except FileExistsError as error:
        raise ConversionError(f"output already exists: {path}") from error
    with os.fdopen(descriptor, "wb") as output:
        output.write(data)


def parse_arguments() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "Create a complete macOS ICNS file. Non-square input is "
            "center-cropped; existing output is never overwritten."
        )
    )
    parser.add_argument("input_image", type=Path)
    parser.add_argument("output", type=Path, nargs="?")
    return parser.parse_args()


def convert(source: Path, output: Path) -> None:
    sips = shutil.which("sips")
    if sips is None:
        raise ConversionError("required macOS command not found: sips")
    if not source.is_file():
        raise ConversionError(f"input is not a readable file: {source}")
    if output.suffix != ".icns":
        raise ConversionError("output path must end in .icns")
    if not output.parent.is_dir():
        raise ConversionError(f"output directory does not exist: {output.parent}")
    if output.exists():
        raise ConversionError(f"output already exists: {output}")

    with tempfile.TemporaryDirectory(prefix="image-to-icns.") as temporary:
        root = Path(temporary)
        master = root / "master.png"
        make_master(sips, source, master, root / "working.png")
        iconset = root / "AppIcon.iconset"
        make_renditions(sips, master, iconset)
        data = build_icns(iconset)

    write_new_file(output, data)
    try:
        properties = run_sips(sips, "-g", "format", output, capture=True)
        if "format: icns" not in properties:
            raise ConversionError("output is not a valid ICNS file")
    except ConversionError:
        output.unlink(missing_ok=True)
        raise


def main() -> int:
    arguments = parse_arguments()
    source = arguments.input_image
    output = arguments.output or source.with_suffix(".icns")
    try:
        convert(source, output)
    except (ConversionError, OSError) as error:
        print(f"image-to-icns: {error}", file=sys.stderr)
        return 1
    print(f"Created {output}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```

