T09 · Insecure Skill Coding Practices
- Location
scripts/create_launcher.py:22- Finding
Hard-Coded FAL API Credential Exposed in Source Code
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
This skill mostly does what it advertises, but it ships a hard-coded external API credential and generates persistent launcher scripts with unsafe path and command handling.
Install only if you are comfortable with a skill that writes persistent macOS .app launchers which run project commands in Terminal. Avoid using --auto-icon unless the embedded FAL credential has been removed and you supply your own approved key. Do not run this on untrusted projects or project paths until app names, paths, and generated commands are validated and safely escaped.
scripts/create_launcher.py:22Hard-Coded FAL API Credential Exposed in Source Code
scripts/create_launcher.py:224Path Traversal Enables Unsafe Recursive Directory Deletion
scripts/create_launcher.py:242Shell and AppleScript Injection Through Unescaped Launcher Values
The declared purpose is only local launcher creation, but the detected behavior includes external network/service usage, remote content downloading, and API credential injection via an environment variable. That mismatch is dangerous because users and policy engines may authorize the skill expecting only local file generation while it can also exfiltrate metadata, fetch untrusted content, or invoke third-party services with hidden credentials.
The code embeds a FAL API credential and silently injects it into the environment at runtime. Hardcoded secrets are inherently unsafe because they can be extracted, reused by unauthorized parties, incur billing abuse, and normalize hidden external access in a skill that appears local-only.
This code reads a hardcoded credential into process environment state without clear user warning or consent. That creates secret-exposure and unauthorized-service-use risk, especially because the skill can trigger network behavior under a seemingly local feature set.
The skill advertises commands that invoke a local Python script to create .app bundles and launch local services, but it declares no explicit tool scope or permissions. In a skill system, missing scope around shell, file write, environment access, and possible network use increases the chance the agent will execute higher-risk actions without clear user-visible constraints or policy enforcement.
The skill is explicitly designed to create a persistent desktop launcher that repeatedly runs a local dev server outside the immediate agent session. Session persistence is risky because it creates an enduring execution path on the user's desktop that can later run commands with a single click, potentially outliving the user's original context or review.
---
name: app-launcher
description: Create macOS desktop app launchers for dev projects. Click an icon on Desktop → auto-runs local dev server in Terminal. Use when user wants to make an app "persistent", create a desktop shortcut, launch dev server with one click, or says "make this app clickable" / "create launcher" / "desktop icon for this project".
homepage: https://canlah.ai
---
The trigger phrases are broad enough to match ordinary requests like making an app clickable or persistent, which can cause the skill to activate in situations where the user did not intend launcher creation or shell execution. Over-broad activation is especially risky here because the skill performs file creation and may run project commands, increasing the likelihood of unintended side effects.
The documented workflow instructs the agent to create a desktop .app that persists after the session, reinforcing the creation of a long-lived execution artifact. In the context of a skill that can auto-detect commands and possibly perform undeclared network-related actions, that persistence increases risk because unsafe behavior can be packaged into a reusable launcher.
## Workflow
1. User says "create launcher for this project" or "make desktop icon"
2. Identify project path and any custom requirements (name, icon, command)
3. Run the script with appropriate options
4. Confirm the .app was created on Desktop
The skill's stated purpose is local launcher creation, but enabling --auto-icon sends app metadata in prompts to a third-party service and downloads remote content without strong disclosure or isolation. This broadens the trust boundary unexpectedly and can leak project/app names or introduce untrusted remote files into the workflow.
The function performs an external network request and downloads remote image content without meaningful disclosure that user-provided app names are transmitted to a third party. In a local-launcher skill, this is an unexpected data flow and could expose sensitive project identities or fetch untrusted content.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
resized.save(output_file, "PNG")
# Convert iconset to icns using iconutil
result = subprocess.run(
["iconutil", "-c", "icns", iconset_dir, "-o", icns_path],
capture_output=True,
text=True
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
launcher_script.write_text(script_content)
launcher_script.chmod(launcher_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Create Info.plist
info_plist = contents_dir / "Info.plist"
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
bundle_id = app_name.lower().replace(" ", "-").replace("_", "-")
plist_content = f'''<?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>CFBundleExecutable</key>
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
sizes = [16, 32, 64, 128, 256, 512]
for size in sizes:
subprocess.run([
"sips", "-z", str(size), str(size),
str(icon_path), "--out", str(iconset_path / f"icon_{size}x{size}.png")
], capture_output=True)
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
str(icon_path), "--out", str(iconset_path / f"icon_{size}x{size}.png")
], capture_output=True)
if size <= 256:
subprocess.run([
"sips", "-z", str(size*2), str(size*2),
str(icon_path), "--out", str(iconset_path / f"icon_{size}x{size}@2x.png")
], capture_output=True)
No suspicious patterns detected.