Generated banner for Launch With AWS
Generated logo for Launch With AWS

Launch With AWS

AWSOfficialPublished by AWS

  • Skill
  • Workflow
  • Cloud and DevOps
  • Apache-2.0
  • Medium risk
  • Has scripts

From the author

Migrates vibe-coded web applications to AWS. Handles the full workflow from analysis through migration to deployment, producing deployable AWS Blocks infrastructure code. Supports full-stack apps built with vibe-coding platforms (Lovable, Bolt.new, Replit) and frontend web applications and websites: React, Vue, Angular, Next.js, Nuxt, Astro, SvelteKit, Gatsby, Vite, Svelte, Solid, Docusaurus, and others (static sites, SPAs, and SSR frameworks with static export). Triggers on: launch with AWS, launch on AWS, deploy to AWS, migrate to AWS, host my app on AWS, move my app to AWS, transfer my app to AWS. Activates when the user wants to migrate a vibe-coded app or frontend web app to AWS, even if they don't say 'migrate' explicitly.
— the description by AWS

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

Launch with AWS provides a structured, automated workflow to migrate full-stack web applications and frontend projects to AWS. It analyzes the source code, generates a migration plan with cost estimates, and produces deployable AWS Blocks infrastructure code. The process includes secure authentication, source code analysis, and a guided execution phase that results in a migration snapshot ready for local application and deployment via generated instructions.

What it does

  • Analyzes web application source code to determine migration requirements.
  • Generates detailed migration plans and AWS cost estimates.
  • Produces AWS Blocks infrastructure code for target applications.
  • Handles secure authentication and session management for AWS migration services.
  • Provides a guided 3-way merge process to integrate migrated code into existing repositories.
  • Generates project-specific deployment instructions via DEPLOY.md.

When to use it

  • When you need to move a vibe-coded or frontend web application to AWS.
  • When you want to transition from platforms like Lovable, Bolt.new, or Replit to AWS-native infrastructure.
  • When you require a structured, step-by-step migration path for static sites, SPAs, or SSR frameworks.
  • When you need to estimate the AWS infrastructure costs for hosting your existing web application.

Advantages

  • Automates the complex process of infrastructure definition and migration planning.
  • Provides clear cost visibility before committing to a migration.
  • Maintains security by using encrypted data handling and avoiding AI training on user code.
  • Supports a wide range of modern web frameworks and platforms.
  • Ensures a clean integration process through git-based migration branches.

Usage examples

“Migrate my Replit web app to AWS.”
The agent initiates the authentication flow, analyzes the Replit project structure, and prepares a migration plan for AWS Lambda and Aurora Serverless.
“How much would it cost to host my Next.js site on AWS?”
The agent analyzes the project, calculates the estimated monthly AWS costs, and presents a breakdown of the required services.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/aws/launch-with-aws/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip launch-with-aws.zip -d .agents/skills

Documentation by AWS

Launch with AWS

Drives an AWS migration end-to-end using CLI scripts. Takes a user's web application, analyzes it, generates a migration plan with cost estimate, and delivers deployable AWS Blocks infrastructure code.

The AWS MCP server is recommended but is not required. This skill works standalone via its CLI scripts in any agent environment.

Script Invocation

All commands are run via:

python3 scripts/launch_with_aws.py <command> [args...]

where scripts/ is relative to this skill directory. The agent MUST set the working directory to the skill root before invoking commands.

Required files: launch_with_aws.py, launch_config.py, auth.py, auth_callback_server.py, launch_api_client.py, archive.py, service model. When loaded via MCP, fetch all and write to a temp directory preserving structure before invoking.

Each command outputs JSON to stdout on success, or exits non-zero with a JSON error on stderr.

Two global flags may be added to any command: --region <region> selects the region the service runs the migration in, and --aws-mcp-url <url> passes the URL of the AWS MCP server the agent is connected to. See step 2.

Dependencies: Python 3.10+ and boto3. The script checks both on startup and exits with a clear error if either is missing.

Supported Application Types

Full-stack apps built with vibe-coding platforms, and frontend web applications and websites (static sites, SPAs, and SSR frameworks with static export).

Origin PlatformWhat it covers
LovableLovable-generated full-stack apps (React + Supabase)
Bolt.newBolt.new-generated full-stack apps (React + Supabase)
ReplitReplit-hosted full-stack apps (React + Express.js + PostgreSQL)
FrameworkExamples
React ecosystemReact, CRA, Vite + React, Gatsby, Docusaurus
Vue ecosystemVue, Nuxt (static export), VitePress
AngularAngular
Svelte ecosystemSvelte, SvelteKit (static export)
SSR with static exportNext.js, Nuxt, Astro, SvelteKit
Other modern frameworksAstro, Solid, Preact, Lit, Eleventy
Vite (generic)Any Vite-based app

Other frameworks may also work. If the user's app doesn't match these, see Unsupported Application Handling below.

What Gets Migrated vs. What Stays

Lovable / Bolt.new apps (Supabase-backed):

ComponentWhat happens
Frontend & hostingMigrated to AWS (S3 + CloudFront + Lambda)
Edge functions / server functionsMigrated to AWS Lambda
AI calls (e.g. Lovable AI Gateway)Migrated to Amazon Bedrock
Database (Supabase DB)Stays on Supabase — not migrated
Auth (Supabase Auth)Stays on Supabase — not migrated
Storage & RealtimeStays on Supabase — not migrated

The app continues to call Supabase for database, auth, storage, and realtime from the AWS-hosted application.

Replit apps (Express.js + PostgreSQL):

ComponentWhat happens
Frontend & hostingMigrated to AWS (S3 + CloudFront + Lambda)
Server logic (Express.js)Migrated to AWS Lambda (API Gateway)
Database (PostgreSQL)Schema and code migrated to AWS (Aurora Serverless / DynamoDB). Existing data is NOT migrated — customers must export and import their data separately.
Auth (Replit Auth)Code migrated to AWS (Cognito). Existing user accounts are NOT migrated — customers must re-create or invite users in Cognito.
Realtime (WebSockets)Migrated to AWS (API Gateway WebSocket)
File storageMigrated to AWS (S3). Existing files are NOT migrated.

Replit app infrastructure and code are migrated to AWS-native services, but existing data, user accounts, and files must be migrated separately by the customer.

Input Resolution

Resolve the user's input to a local directory path:

  • If the user provides a local path: pass that path directly.

  • If the user provides a GitHub URL: clone the repository locally and use that directory.

  • If neither is provided: use the current working directory. If it doesn't look like an app directory, ask the user for the path.

Flow

Run the script commands in order, surfacing results to the user at each step:

1. Authentication
python3 scripts/launch_with_aws.py auth-start

Always run first. Returns immediately with JSON:

  • If already authenticated: {"authenticated": true, "reusedCachedSession": true, "baseUrl": "..."}

  • If silent refresh succeeded: {"authenticated": true, "reusedCachedSession": false, "baseUrl": "..."}

  • If interactive sign-in is needed: {"authenticated": false, "signInUrl": "https://...", "pid": 12345, "port": 54321, "baseUrl": "..."}

When authenticated is false, immediately display the signInUrl to the user (so they can open it in their browser) and call auth-wait in the same response:

python3 scripts/launch_with_aws.py auth-wait <pid>

where <pid> is the pid value from the auth-start response. This blocks until the user completes browser sign-in (or times out after 600s). Returns {"authenticated": true, "baseUrl": "..."} on success.

Sessions are capped at 90 days even if the identity provider does not set an expiration; after that the interactive flow is required again.

To check the current session without authenticating, or to sign out:

python3 scripts/launch_with_aws.py session-status
python3 scripts/launch_with_aws.py sign-out

session-status reports whether a session exists and how long until the token and overall session expire. sign-out best-effort revokes the refresh token and deletes the local ~/.launch-with-aws/session.json. On shared or untrusted workstations, run sign-out when finished.

2. Create Launch

For a local directory, present this data-handling confirmation verbatim and wait for explicit approval:

Your source code will be uploaded to the Launch with AWS service in [regionName], [region] ([sourceLabel]) to analyze your application and generate a migration plan. If you later approve execution, an AWS-hosted agent will modify a copy of your source code according to the plan and produce a migrated snapshot for you to download. Your uploaded source code and associated launch data are encrypted in transit and at rest and retained for up to 48 hours for recovery. Your data is never used to train AI models. We exclude Git history, Git-ignored files, and files matching common sensitive-file patterns. Sensitive-file filtering is best effort; review your project for secrets. Continue? Or reply with [otherRegions regionName] to run the migration there instead.

Fill in the region fields from resolve-region, verbatim, and offer each otherRegions entry as an alternative. If the agent is connected to an AWS MCP server, pass that server's URL verbatim. The script reads the region from it and decides whether it counts as a signal.

python3 scripts/launch_with_aws.py resolve-region [--aws-mcp-url https://aws-mcp.<region>.api.aws/mcp]

Returns {"region": ..., "regionName": ..., "source": ..., "sourceLabel": ..., "baseUrl": ..., "otherRegions": [{"region": ..., "regionName": ...}]}.

Do NOT call create-launch until the user explicitly confirms; a missing or ambiguous response means no. Naming one of otherRegions is both a region choice and approval — pass its region as --region and do not ask again.

python3 scripts/launch_with_aws.py create-launch <app-directory> [name] --region <region>

Zips the directory, uploads it, then creates the launch. Returns the full launch object including launch.launchId and the region it was created in, which becomes the user's saved default. A launch exists only in its own region — pass its --region on every later command.

The launch starts in analyzing status and automatically progresses through analysis and planning.

3. Poll Launch Status
python3 scripts/launch_with_aws.py get-launch-status <launch-id> --region <region>

Poll until status is planned (ready for execution) or failed; report any other status to the user. Key status progression:

  • analyzing → detecting app type and dependencies

  • planning → generating migration plan

  • planned → ready for execution

  • executing → deployment in progress

  • completed → done

  • failed → check failureReason

4. Get Full Launch Details & Confirm
python3 scripts/launch_with_aws.py get-launch <launch-id> plan,cost_estimate --region <region>

Get full launch details. Optional second argument is a comma-separated include list: analysis, plan, execution, cost_estimate, download_url.

Present the cost estimate and plan to the user. The costEstimate field in the response contains estimatedMonthlyCost, region, and a services breakdown with per-service costs.

Confirmation Gate — present and wait for explicit approval:

Migration Summary

  • App type: [detected type from analysis]

  • Architecture: [target architecture from plan]

  • Estimated monthly cost: $X.XX/month ([costEstimate.region] pricing)

  • Migration runs in: [regionName], [region]

Ready to proceed? This will execute the migration in an AWS-managed environment (no cost to you) and produce the migrated snapshot for you to download.

Do NOT call start-launch-execution until the user explicitly confirms.

5. Start Execution
python3 scripts/launch_with_aws.py start-launch-execution <launch-id> --region <region>

Starts deployment. Then poll with get-launch-status until status is completed or failed. Sleep at least 30 seconds between polls.

6. Download
python3 scripts/launch_with_aws.py get-launch-download-url <launch-id> --region <region>

Always present the full download URL to the user — they may need it to download the migrated snapshot directly or for reference.

7. List or Delete Launches
python3 scripts/launch_with_aws.py list-launches
python3 scripts/launch_with_aws.py delete-launch <launch-id> --region <region>

list-launches queries both regions and returns items with a region on each launch; add --region to query one. A region that cannot be reached is reported under errors instead of failing the command, unless every queried region fails, in which case the command exits with that error.

8. Post-Migration: Apply Migrated Code Locally

After obtaining the download URL (adapt commands for the user's platform if not POSIX):

Step A: Download and unpack
curl -L -o /tmp/migration-snapshot.zip "<download_url>"
mkdir -p /tmp/migration-output
unzip -o /tmp/migration-snapshot.zip -d /tmp/migration-output
Step B: Prepare the local workspace

Ensure the user's working directory is clean:

cd <user-app-directory>
git status

If there are uncommitted changes, ask the user to commit or stash first. Do NOT proceed with a dirty working tree.

Step C: Apply migration (3-way merge)

Create a migration branch and overlay the migrated files:

cd <user-app-directory>
git checkout -b aws-migration
rsync -a /tmp/migration-output/ .
git status
git diff --stat

Review the changes with the user. Key additions to highlight:

  • aws-blocks/ — AWS Blocks infrastructure definition

  • DEPLOY.md — deployment instructions

  • Any modified config files

If there are conflicts with the user's existing files, present them and ask how to resolve.

Step D: Follow DEPLOY.md

Read the DEPLOY.md file in the project root and follow its instructions to deploy the app to the user's AWS account. Typical steps:

  1. AWS authentication (aws login --profile aws-migrate --region us-east-1)

  2. CDK bootstrap (first-time only): npm install && npx cdk bootstrap

  3. Deploy: npx cdk deploy --all --progress events

  4. Verify the CloudFront URL that CDK prints on completion.

Important: Always read DEPLOY.md from the migrated output — it is generated specifically for this app and architecture. Do not assume deployment steps from memory.

Unsupported Application Handling

If a launch fails during analysis with a failureReason indicating an unsupported app type (or the user's stack doesn't match the supported list):

  1. Tell the user: "This app type isn't directly supported by Launch with AWS yet. Let me search for other skills that can help deploy this kind of application."

  2. Search for relevant skills based on the app type (e.g. aws-serverless, aws-containers, aws-database, aws-deployment, aws-cdk, aws-ai-ml).

Files

scripts/archive.py

# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""Build the repo zip the backend expects."""

import io
import logging
import os
import re
import stat
import subprocess
import tempfile
import urllib.error
import urllib.request
import zipfile
from typing import Optional, Set, Tuple
from urllib.parse import urlparse

from launch_config import (
    GITHUB_ZIPBALL_TIMEOUT_SECS,
    MAX_ARCHIVE_BYTES,
    MAX_ARCHIVE_ENTRIES,
    MAX_COMPRESSION_RATIO,
    MAX_ENTRY_UNCOMPRESSED_BYTES,
    MAX_UNCOMPRESSED_BYTES,
)

logger = logging.getLogger(__name__)

# Chunk size for reading the GitHub download.
_DOWNLOAD_CHUNK_BYTES = 64 * 1024

# Hosts a GitHub zipball request may be redirected through: api.github.com
# redirects to codeload and then to its S3-backed object store.
_ALLOWED_REDIRECT_HOST_SUFFIXES = (
    ".github.com",
    ".githubusercontent.com",
    ".amazonaws.com",
)
_ALLOWED_REDIRECT_HOSTS = frozenset({"github.com"})

_SKIP_DIRS = frozenset(
    {
        ".git",
        ".hg",
        ".svn",
        "node_modules",
        ".next",
        ".nuxt",
        ".turbo",
        ".cache",
        ".parcel-cache",
        "dist",
        "build",
        "out",
        "coverage",
        ".venv",
        "venv",
        "__pycache__",
        ".pytest_cache",
        ".mypy_cache",
        ".ruff_cache",
        ".idea",
        ".vscode",
        # Credential/secret directories
        ".aws",
        ".ssh",
        ".gnupg",
        ".gcp",
        ".azure",
        ".docker",
        ".kube",
    }
)

_SKIP_FILES = frozenset(
    {
        ".env",
        ".env.local",
        ".env.production",
        ".env.development",
        ".env.test",
        ".env.staging",
        "credentials",
        "credentials.json",
        ".git-credentials",
        ".netrc",
        ".pypirc",
        ".npmrc",
        ".htpasswd",
    }
)

# Best-effort filtering of common credential files, not a security boundary.
# A filtered archive is not guaranteed to be secret-free.
_SKIP_SUFFIXES = (".pem", ".key", ".p12", ".pfx", ".jks", ".keystore")

_SKIP_NAME_PREFIXES = ("id_rsa", "id_ed25519", "id_ecdsa", "id_dsa")

_GITHUB_URL_RE = re.compile(
    r"^https://github\.com/([\w.-]+)/([\w.-]+?)(?:\.git)?/?$",
)


class ArchiveError(Exception):
    """Raised when a repo archive cannot be produced."""


def _decode_git_paths(output: bytes) -> Set[str]:
    """Decode NUL-delimited Git paths using the filesystem encoding."""
    return {os.fsdecode(path) for path in output.split(b"\0") if path}


def _run_git(root: str, args: list[str]) -> subprocess.CompletedProcess:
    """Run a Git file-enumeration command with consistent safeguards."""
    return subprocess.run(
        ["git", *args],
        cwd=root,
        capture_output=True,
        timeout=30,
    )


def _has_gitignore(root: str) -> bool:
    """Return whether the source tree contains an applicable .gitignore file."""
    for _, dirnames, filenames in os.walk(root):
        dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
        if ".gitignore" in filenames:
            return True
    return False


def _gitignore_files_without_repo(root: str) -> Set[str]:
    """Apply Git ignore rules to a directory that is not a Git worktree."""
    with tempfile.TemporaryDirectory(prefix="launch-with-aws-git-") as git_dir:
        initialized = _run_git(root, ["init", "--bare", "--quiet", git_dir])
        if initialized.returncode != 0:
            raise ArchiveError("Git could not initialize temporary metadata to apply ignore rules.")

        result = _run_git(
            root,
            [
                f"--git-dir={git_dir}",
                f"--work-tree={root}",
                "ls-files",
                "--others",
                "--exclude-standard",
                "-z",
            ],
        )
        if result.returncode != 0:
            raise ArchiveError(
                "Git could not determine which files are safe to upload. "
                "Resolve the Git error and try again."
            )
        return _decode_git_paths(result.stdout)


def _git_included_files(root: str) -> Optional[Set[str]]:
    """Return paths selected by Git ignore rules, or None when no rules apply.

    NUL-delimited output preserves unusual filenames. Tracked files are combined
    with untracked-but-not-ignored files so new local files are included.
    """
    try:
        result = _run_git(
            root,
            [
                "ls-files",
                "--cached",
                "--others",
                "--exclude-standard",
                "-z",
            ],
        )
        if result.returncode == 0:
            ignored_tracked = _run_git(
                root,
                [
                    "ls-files",
                    "--cached",
                    "--ignored",
                    "--exclude-standard",
                    "-z",
                ],
            )
            if ignored_tracked.returncode != 0:
                raise ArchiveError(
                    "Git could not determine which tracked files match ignore rules."
                )
            return _decode_git_paths(result.stdout) - _decode_git_paths(ignored_tracked.stdout)
        if not _has_git_metadata(root):
            if _has_gitignore(root):
                return _gitignore_files_without_repo(root)
            return None
        raise ArchiveError(
            "Git could not determine which files are safe to upload. "
            "Resolve the Git error and try again."
        )
    except subprocess.TimeoutExpired as err:
        raise ArchiveError(
            "Git timed out while determining which files are safe to upload."
        ) from err
    except OSError as err:
        if _has_git_metadata(root) or _has_gitignore(root):
            raise ArchiveError(
                "Git is required to apply this source directory's ignore rules before upload."
            ) from err
        return None


def _has_git_metadata(root: str) -> bool:
    """Return whether root is at or below a directory containing Git metadata."""
    current = os.path.abspath(root)
    while True:
        if os.path.exists(os.path.join(current, ".git")):
            return True
        parent = os.path.dirname(current)
        if parent == current:
            return False
        current = parent


def parse_github_url(url: str) -> Optional[Tuple[str, str]]:
    """Return (owner, repo) for a GitHub HTTPS URL, or None.

    Only https://github.com/<owner>/<repo> URLs are accepted, and `.` / `..`
    segments are rejected.
    """
    match = _GITHUB_URL_RE.match(url.strip())
    if not match:
        return None
    owner, repo = match.group(1), match.group(2)
    if any(segment in (".", "..") for segment in (owner, repo)):
        return None
    return owner, repo


def sanitize_root_name(name: str) -> str:
    segments = [s for s in re.split(r"[\\/]", name.strip()) if s]
    base = (segments[-1] if segments else "").strip().lstrip(".")
    cleaned = re.sub(r"[^\w.-]", "-", base).strip("-")
    return cleaned or "app"


def _is_secret_file(filename: str) -> bool:
    """Return True if the filename matches a known secret pattern."""
    lower = filename.lower()
    if lower in _SKIP_FILES:
        return True
    if lower.startswith(".env."):
        return True
    if lower.endswith(_SKIP_SUFFIXES):
        return True
    if any(lower.startswith(p) for p in _SKIP_NAME_PREFIXES):
        return True
    return False


def _is_safe_entry_name(name: str) -> bool:
    """Return True if a ZIP entry name is a plain relative path.

    Rejects absolute paths, Windows drive/UNC prefixes, `..` segments, and
    embedded NUL bytes.
    """
    if not name or "\x00" in name:
        return False
    if name.startswith("/") or name.startswith("\\"):
        return False
    # Windows drive letter (e.g. "C:\") or UNC ("\\host").
    if re.match(r"^[A-Za-z]:", name) or name.startswith("\\\\"):
        return False
    normalized = name.replace("\\", "/")
    return not any(segment == ".." for segment in normalized.split("/"))


def _is_symlink_entry(info: zipfile.ZipInfo) -> bool:
    """Return True if a ZIP entry encodes a Unix symlink."""
    # The high 16 bits of external_attr hold the Unix mode for Unix-created
    # entries.
    mode = (info.external_attr >> 16) & 0xFFFF
    return stat.S_ISLNK(mode)


def validate_archive_bytes(data: bytes) -> None:
    """Validate ZIP entry names, shape, and size limits.

    All checks read the central-directory metadata only (nothing is
    decompressed). Raises ArchiveError on the first violation.
    """
    try:
        zf = zipfile.ZipFile(io.BytesIO(data))
    except zipfile.BadZipFile as err:
        raise ArchiveError("Downloaded file is not a valid zip archive.") from err

    with zf:
        infos = zf.infolist()

        if len(infos) > MAX_ARCHIVE_ENTRIES:
            raise ArchiveError(
                f"Archive has {len(infos)} entries, exceeding the "
                f"{MAX_ARCHIVE_ENTRIES} entry limit."
            )

        total_uncompressed = 0
        roots: Set[str] = set()
        for info in infos:
            name = info.filename

            if not _is_safe_entry_name(name):
                raise ArchiveError(f"Archive contains an unsafe entry path: {name!r}")
            if _is_symlink_entry(info):
                raise ArchiveError(f"Archive contains a symlink entry: {name!r}")

            size = info.file_size
            if size > MAX_ENTRY_UNCOMPRESSED_BYTES:
                raise ArchiveError(
                    f"Archive entry {name!r} is {size // (1024 * 1024)} MiB "
                    f"uncompressed, exceeding the "
                    f"{MAX_ENTRY_UNCOMPRESSED_BYTES // (1024 * 1024)} MiB per-entry limit."
                )
            if info.compress_size > 0 and size / info.compress_size > MAX_COMPRESSION_RATIO:
                raise ArchiveError(
                    f"Archive entry {name!r} exceeds the {MAX_COMPRESSION_RATIO}:1 "
                    "compression-ratio limit."
                )

            total_uncompressed += size
            if total_uncompressed > MAX_UNCOMPRESSED_BYTES:
                raise ArchiveError(
                    f"Archive decompresses to over "
                    f"{MAX_UNCOMPRESSED_BYTES // (1024 * 1024 * 1024)} GiB, "
                    "exceeding the decompressed-size limit."
                )

            top = info.filename.replace("\\", "/").split("/", 1)[0]
            if top:
                roots.add(top)

        if len(roots) > 1:
            raise ArchiveError(
                f"Archive must contain a single top-level directory; found {len(roots)}."
            )


def zip_local_repo(path: str, root_name: str) -> bytes:
    """Zip a local repo directory, skipping VCS/build artifacts and secrets.

    When the directory is a git repository, .gitignore rules are respected
    via `git ls-files`. The hardcoded secret exclusions still apply on top
    as a defense-in-depth measure.
    """
    root = os.path.abspath(os.path.expanduser(path))
    if not os.path.isdir(root):
        raise ArchiveError(f"Not a directory: {path}")

    prefix = sanitize_root_name(root_name)
    skipped: list[str] = []
    git_files = _git_included_files(root)

    if git_files is not None:
        logger.info("Using git to determine included files (.gitignore respected)")

    buffer = io.BytesIO()
    file_count = 0
    with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
        for dirpath, dirnames, filenames in os.walk(root):
            dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
            for filename in filenames:
                abs_path = os.path.join(dirpath, filename)
                if os.path.islink(abs_path):
                    continue
                rel = os.path.relpath(abs_path, root).replace(os.sep, "/")
                if git_files is not None and rel not in git_files:
                    continue
                if _is_secret_file(filename):
                    skipped.append(rel)
                    continue
                zf.write(abs_path, f"{prefix}/{rel}")
                file_count += 1

    if skipped:
        logger.info(
            "Excluded %d file(s) matching secret patterns: %s",
            len(skipped),
            ", ".join(skipped),
        )

    if file_count == 0:
        raise ArchiveError(f"No files found under {path}")

    data = buffer.getvalue()
    if len(data) > MAX_ARCHIVE_BYTES:
        raise ArchiveError(
            f"Archive is {len(data) // (1024 * 1024)} MiB, exceeding the "
            f"{MAX_ARCHIVE_BYTES // (1024 * 1024)} MiB limit. Remove large "
            "files or build artifacts and try again."
        )
    validate_archive_bytes(data)
    return data


def _is_allowed_redirect_host(hostname: Optional[str]) -> bool:
    """Return True if a redirect target host is on the allowlist."""
    if not hostname:
        return False
    hostname = hostname.lower()
    if hostname in _ALLOWED_REDIRECT_HOSTS:
        return True
    return any(hostname.endswith(suffix) for suffix in _ALLOWED_REDIRECT_HOST_SUFFIXES)


class _AllowlistRedirectHandler(urllib.request.HTTPRedirectHandler):
    """Redirect handler that only follows redirects to allowlisted hosts.

    GitHub zipball downloads 302 from api.github.com to codeload and then to an
    S3 object store; redirects to any other host are refused.
    """

    def redirect_request(self, req, fp, code, msg, headers, newurl):
        parsed = urlparse(newurl)
        if parsed.scheme != "https":
            raise ArchiveError(f"Refusing redirect to non-HTTPS URL during download: {newurl!r}")
        if not _is_allowed_redirect_host(parsed.hostname):
            raise ArchiveError(
                f"Refusing redirect to disallowed host during download: {parsed.hostname!r}"
            )
        return super().redirect_request(req, fp, code, msg, headers, newurl)


def _read_capped(resp) -> bytes:
    """Read a response body, aborting if it exceeds MAX_ARCHIVE_BYTES.

    Pre-checks the declared Content-Length when present, then reads in chunks
    against a running total so bodies without a Content-Length are also bounded.
    """
    declared = resp.headers.get("Content-Length")
    if declared is not None:
        try:
            if int(declared) > MAX_ARCHIVE_BYTES:
                raise ArchiveError(
                    f"Downloaded archive is {int(declared) // (1024 * 1024)} MiB, "
                    f"exceeding the {MAX_ARCHIVE_BYTES // (1024 * 1024)} MiB limit."
                )
        except ValueError:
            pass

    chunks: list[bytes] = []
    total = 0
    while True:
        chunk = resp.read(_DOWNLOAD_CHUNK_BYTES)
        if not chunk:
            break
        total += len(chunk)
        if total > MAX_ARCHIVE_BYTES:
            raise ArchiveError(
                f"Downloaded archive exceeds the "
                f"{MAX_ARCHIVE_BYTES // (1024 * 1024)} MiB limit."
            )
        chunks.append(chunk)
    return b"".join(chunks)


def download_github_zip(url: str) -> bytes:
    """Download and validate a public GitHub repo's default-branch archive.

    Follows only allowlisted redirects, caps the body at MAX_ARCHIVE_BYTES, and
    validates the archive before returning the bytes.
    """
    parsed = parse_github_url(url)
    if not parsed:
        raise ArchiveError(f"Not a GitHub repository URL: {url}")
    owner, repo = parsed

    zipball = f"https://api.github.com/repos/{owner}/{repo}/zipball"
    req = urllib.request.Request(zipball, headers={"Accept": "application/vnd.github+json"})
    opener = urllib.request.build_opener(_AllowlistRedirectHandler())
    try:
        with opener.open(req, timeout=GITHUB_ZIPBALL_TIMEOUT_SECS) as resp:
            data = _read_capped(resp)
    except ArchiveError:
        raise
    except urllib.error.HTTPError as err:
        if err.code in (401, 403, 404):
            raise ArchiveError(
                f"Could not download {owner}/{repo}. Only public repositories can "
                "be fetched by URL — for a private repo, clone it locally and pass "
                "the local path instead."
            ) from err
        raise ArchiveError(f"GitHub returned {err.code} downloading {owner}/{repo}.") from err
    except (urllib.error.URLError, OSError) as err:
        raise ArchiveError(f"Failed to download {url}: {err}") from err

    validate_archive_bytes(data)
    return data

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server, Needs a command-line tool, Needs Python, Needs Node.js

Security

What our scanner noticed· Medium risk

  • Phrasing that asks for actions without confirmation
  • References to credential files
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
AWS
Publisher
AWS (Official)
Source
aws/agent-toolkit-for-aws · skills/core-skills/launch-with-aws · commit 0a218f8
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • NOTICE (added by AutoGineer from NOTICE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced
Also distributed in
aws/agent-toolkit-for-aws

AutoGineer is not affiliated with or endorsed by AWS or AWS.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download launch-with-aws.zip