Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions skills/namecheap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ DO NOT USE FOR: domain registration/purchase, SSL certificate management, hostin

Before executing any API commands, verify credentials are configured:

1. **Check for existing config** — look for `~/.namecheap-api`
1. **Check for existing config** — look for `~/.namecheap-api` and ensure `NAMECHEAP_API_KEY` is exported in the user's shell
2. If not configured, guide the user through setup:
a. **Show public IP** — run `python3 namecheap.py public-ip` to display the user's public IP
b. **Instruct IP whitelisting** — tell the user to go to https://ap.www.namecheap.com/settings/tools/apiaccess/, enable API (select ON), and whitelist the displayed IP
c. **Have the user run setup themselves** — ask the user to run `python3 namecheap.py setup` directly **in their own terminal**. The script prompts for the username and reads the API key with a hidden prompt (`getpass`), writes `~/.namecheap-api` with `chmod 600`, and validates the connection. **Never ask the user to paste their API key into the chat, and never log, echo, or display the API key value.** If you cannot run an interactive terminal for the user, instruct them to run `setup` themselves, or to export `NAMECHEAP_API_USER` and `NAMECHEAP_API_KEY` as environment variables in their own shell — rather than collecting the secret via `ask_user`.
c. **Have the user run setup themselves** — ask the user to run `python3 namecheap.py setup` directly **in their own terminal**. The script prompts for the username, reads the API key with a hidden prompt (`getpass`), saves only the username to `~/.namecheap-api` with `chmod 600`, and validates the connection without writing the API key to disk. **Never ask the user to paste their API key into the chat, and never log, echo, or display the API key value.** After setup, the user must export `NAMECHEAP_API_KEY` in their own shell before running API commands. If you cannot run an interactive terminal for the user, instruct them to run `setup` themselves, or to export `NAMECHEAP_API_USER` and `NAMECHEAP_API_KEY` in their own shell — rather than collecting the secret via `ask_user`.
d. **Confirm** — once the user reports setup succeeded, proceed with DNS operations.

### DNS Operations
Expand Down Expand Up @@ -102,7 +102,7 @@ python3 namecheap.py domains.ns.update --domain example.com --nameserver ns1.exa

## Behavior

- **Always check credentials first.** Before any API operation, verify `~/.namecheap-api` exists and is readable. If not, run the setup flow.
- **Always check credentials first.** Before any API operation, verify `~/.namecheap-api` exists and is readable, and that `NAMECHEAP_API_KEY` is set in the current shell. If not, run the setup flow and have the user export the API key.
- **Show current records before modifying.** Before adding or removing records, always fetch and display the current DNS records so the user can confirm the change.
- **Use `ask_user` to confirm destructive changes.** Before removing records or replacing all records with `setHosts`, confirm with the user.
- **The Namecheap `setHosts` API replaces ALL records.** Never call `domains.dns.setHosts` directly unless you have fetched all existing records first. Use `dns.addHost` and `dns.removeHost` for safe single-record operations — they handle the fetch-modify-write cycle internally.
Expand All @@ -111,14 +111,13 @@ python3 namecheap.py domains.ns.update --domain example.com --nameserver ns1.exa

## Credential Storage

Credentials are stored in `~/.namecheap-api`:
The Namecheap username is stored in `~/.namecheap-api`:

```bash
NAMECHEAP_API_USER="username"
NAMECHEAP_API_KEY="api-key-here"
```

This file must have `600` permissions (owner read/write only). Alternatively, the script reads credentials from the `NAMECHEAP_API_USER` and `NAMECHEAP_API_KEY` environment variables, which take precedence over the file when both are set.
This file must have `600` permissions (owner read/write only). The API key is **not** stored on disk; the script reads `NAMECHEAP_API_KEY` from the environment and uses it only in memory. `NAMECHEAP_API_USER` may be supplied either in the file or in the environment, and environment variables take precedence when both are set. If a legacy `NAMECHEAP_API_KEY` line already exists in `~/.namecheap-api`, it should be removed after the user exports the key securely in their shell.

## Supported Record Types

Expand Down
176 changes: 126 additions & 50 deletions skills/namecheap/namecheap.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#!/usr/bin/env python3
"""Namecheap API CLI wrapper for DNS management.

Uses only the Python standard library (no third-party dependencies). Credentials
are read from ``~/.namecheap-api`` (or env vars) and are never passed on the
command line, so they cannot leak via ``ps``/shell history.
Uses only the Python standard library (no third-party dependencies). The
username can be cached in ``~/.namecheap-api`` while the API key is read from
the ``NAMECHEAP_API_KEY`` environment variable (or prompted during setup) and
is never passed on the command line, so it cannot leak via ``ps``/shell
history.
"""

import argparse
Expand Down Expand Up @@ -75,47 +77,107 @@ class NamecheapError(Exception):

# --- Configuration --------------------------------------------------------

def _redact_sensitive_text(text):
text = re.sub(r'(?i)(ApiKey=)[^&\s]+', r"\1[REDACTED]", text)
text = re.sub(r'(?i)(NAMECHEAP_API_KEY\s*=\s*"?)[^"\n]+("?)', r"\1[REDACTED]\2", text)
return text

def _read_config_values():
"""Return shell-style KEY="value" pairs from the local config file."""
if not os.path.isfile(CONFIG_FILE):
return {}

with open(CONFIG_FILE, "r", encoding="utf-8") as fh:
content = fh.read()

pattern = re.compile(r'^\s*([A-Z_]+)\s*=\s*"?([^"\n]*)"?\s*$', re.MULTILINE)
return {m.group(1): m.group(2) for m in pattern.finditer(content)}


def load_config():
"""Return (api_user, api_key), preferring env vars then the config file."""
"""Return (api_user, api_key), preferring env vars for sensitive values."""
api_user = os.environ.get("NAMECHEAP_API_USER")
api_key = os.environ.get("NAMECHEAP_API_KEY")
if api_user and api_key:
return api_user, api_key

if os.path.isfile(CONFIG_FILE):
with open(CONFIG_FILE, "r", encoding="utf-8") as fh:
content = fh.read()
# File uses shell-style KEY="value" lines for backward compatibility.
pattern = re.compile(r'^\s*([A-Z_]+)\s*=\s*"?([^"\n]*)"?\s*$', re.MULTILINE)
values = {m.group(1): m.group(2) for m in pattern.finditer(content)}
api_user = api_user or values.get("NAMECHEAP_API_USER")
api_key = api_key or values.get("NAMECHEAP_API_KEY")

values = _read_config_values()
api_user = api_user or values.get("NAMECHEAP_API_USER")

return api_user, api_key


def check_credentials():
api_user, api_key = load_config()
if not api_user or not api_key:
err("Namecheap API credentials not configured.")
print()
print("Run 'python3 namecheap.py setup' to configure your credentials.")
print()
print("You need:")
print(" 1. Your Namecheap username")
print(" 2. An API key from: https://ap.www.namecheap.com/settings/tools/apiaccess/")
print(" 3. Your public IP whitelisted in the API settings")
sys.exit(1)
return api_user, api_key
def has_legacy_api_key():
"""Return True when a legacy clear-text API key is still present on disk."""
return bool(_read_config_values().get("NAMECHEAP_API_KEY"))


def save_config(api_user, api_key):
def save_config(api_user):
with open(CONFIG_FILE, "w", encoding="utf-8") as fh:
fh.write("# Cached Namecheap username for this skill.\n")
fh.write("# Export NAMECHEAP_API_KEY in your shell before running API commands.\n")
fh.write(f'NAMECHEAP_API_USER="{api_user}"\n')
fh.write(f'NAMECHEAP_API_KEY="{api_key}"\n')
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 600


def print_safe_api_failure(prefix, public_ip=None):
err(prefix)
print("Please verify:")
print(" 1. API access is enabled (ON) at the Namecheap settings page")
if public_ip:
print(f" 2. IP address {public_ip} is whitelisted")
else:
print(" 2. Your current public IP is whitelisted")
print(" 3. Your username and API key are correct")


def print_safe_namecheap_error(exc):
message = _redact_sensitive_text(str(exc)).strip()
if message:
err(f"API returned error: {message}")
else:
err("API returned an error.")


def print_safe_runtime_error(exc):
if isinstance(exc, urllib.error.URLError):
err("Network error while contacting Namecheap or resolving your public IP.")
return
if isinstance(exc, OSError):
err(f"Could not read or update {CONFIG_FILE}.")
return
if isinstance(exc, ET.ParseError):
err("Received an invalid XML response from the Namecheap API.")
return
if isinstance(exc, json.JSONDecodeError):
err("Could not parse the JSON input file that was provided.")
return
err("The operation failed.")


def print_credential_help(legacy_key_present=False):
err("Namecheap API credentials are not configured.")
print()
print("Run 'python3 namecheap.py setup' to save your Namecheap username.")
print("Then export NAMECHEAP_API_KEY in your own shell before running API commands.")
print()
print("You need:")
print(" 1. Your Namecheap username")
print(" 2. An API key from: https://ap.www.namecheap.com/settings/tools/apiaccess/")
print(" 3. Your public IP whitelisted in the API settings")
if legacy_key_present:
print()
warn(f"A legacy API key entry in {CONFIG_FILE} is ignored for security reasons.")
print("Remove the NAMECHEAP_API_KEY line from that file after exporting the key securely.")


def check_credentials():
api_user, api_key = load_config()
legacy_key_present = has_legacy_api_key()
if not api_user or not api_key:
print_credential_help(legacy_key_present)
sys.exit(1)
return api_user, api_key
Comment thread
brunoborges marked this conversation as resolved.


# --- Networking -----------------------------------------------------------

def _http_get(url, timeout=15):
Expand Down Expand Up @@ -224,45 +286,59 @@ def cmd_setup(_args):
print()

existing_user, existing_key = load_config()
if existing_user and existing_key:
if has_legacy_api_key():
warn(f"A legacy API key was found in {CONFIG_FILE} and will no longer be used.")
print("Run setup to rewrite the file without the API key, then export NAMECHEAP_API_KEY securely.")
print()

if existing_user:
info(f"Existing configuration found for user: {existing_user}")
print("\nTesting API connection...")
try:
api_request("domains.getList", {"PageSize": "1"})
success("API connection successful!")
except Exception as exc: # noqa: BLE001
err(f"API connection failed: {exc}")
if existing_key:
print("\nTesting API connection...")
try:
api_request("domains.getList", {"PageSize": "1"})
success("API connection successful!")
except NamecheapError:
print_safe_api_failure("API connection failed.", public_ip)
except (urllib.error.URLError, OSError, ET.ParseError, json.JSONDecodeError) as exc:
print_safe_runtime_error(exc)
else:
info("No NAMECHEAP_API_KEY environment variable is set yet.")
print("The setup flow will validate the API key you enter, but it will not write the key to disk.")
print()
answer = input("Update stored credentials? [y/N]: ").strip().lower()
answer = input("Update saved username or validate a new API key? [y/N]: ").strip().lower()
if answer not in ("y", "yes"):
info("Keeping existing credentials.")
return
print()

print("Enter your Namecheap credentials:\n")
api_user = input(" API Username: ").strip()
prompt = " API Username"
if existing_user:
prompt += f" [{existing_user}]"
prompt += ": "
api_user = input(prompt).strip() or existing_user or ""
api_key = getpass.getpass(" API Key (hidden): ").strip()
print()

if not api_user or not api_key:
err("Both username and API key are required.")
sys.exit(1)

save_config(api_user, api_key)
success(f"Credentials saved to {CONFIG_FILE}")
save_config(api_user)
success(f"Username saved to {CONFIG_FILE}")
info("The API key was not written to disk. Export NAMECHEAP_API_KEY in your shell for future commands.")
print("\nTesting API connection...")
try:
# Use the just-entered credentials directly for the validation call.
os.environ["NAMECHEAP_API_USER"] = api_user
os.environ["NAMECHEAP_API_KEY"] = api_key
api_request("domains.getList", {"PageSize": "1"})
success("API connection successful!")
except Exception as exc: # noqa: BLE001
warn("API connection failed. Please verify:")
print(" 1. API access is enabled (ON) at the Namecheap settings page")
print(f" 2. IP address {public_ip} is whitelisted")
print(" 3. Your API key is correct")
print(f" (details: {exc})")
except NamecheapError:
print_safe_api_failure("API connection failed.", public_ip)
except (urllib.error.URLError, OSError, ET.ParseError, json.JSONDecodeError) as exc:
print_safe_runtime_error(exc)


def cmd_domains_list(args):
Expand Down Expand Up @@ -682,13 +758,13 @@ def main(argv=None):
try:
args.func(args)
except NamecheapError as exc:
err(f"API returned error: {exc}")
print_safe_namecheap_error(exc)
return 1
except urllib.error.URLError as exc:
err(f"Network error: {exc}")
print_safe_runtime_error(exc)
return 1
except (OSError, ET.ParseError, json.JSONDecodeError) as exc:
err(str(exc))
print_safe_runtime_error(exc)
return 1
return 0

Expand Down
Loading