---
phase: 05-secrets-management
plan: 02
type: execute
wave: 2
depends_on: [05-01]
files_modified:
  - src/clawrium/cli/secret.py
  - src/clawrium/cli/main.py
  - tests/test_cli_secret.py
autonomous: true
requirements: [SEC-01, SEC-02, SEC-03]
must_haves:
  truths:
    - "User can set a secret with `clm secret set KEY` using masked input"
    - "User can list secret keys (not values) with `clm secret list`"
    - "Missing required secrets shown grouped by claw type in list output"
    - "User can remove a secret with confirmation via `clm secret remove KEY`"
    - "Values never displayed in CLI output"
  artifacts:
    - path: "src/clawrium/cli/secret.py"
      provides: "CLI commands for secret management (set, list, remove)"
      exports: ["secret_app"]
    - path: "src/clawrium/cli/main.py"
      provides: "Main CLI with secret subcommand registered"
      contains: "app.add_typer(secret_app"
    - path: "tests/test_cli_secret.py"
      provides: "CLI tests for secret commands"
      min_lines: 80
  key_links:
    - from: "src/clawrium/cli/secret.py"
      to: "src/clawrium/core/secrets.py"
      via: "import get_secret, set_secret, remove_secret, list_secrets"
      pattern: "from clawrium\\.core\\.secrets import"
    - from: "src/clawrium/cli/secret.py"
      to: "src/clawrium/core/registry.py"
      via: "get_required_secrets for missing secrets display"
      pattern: "from clawrium\\.core\\.registry import"
    - from: "src/clawrium/cli/main.py"
      to: "src/clawrium/cli/secret.py"
      via: "app.add_typer(secret_app)"
      pattern: "from clawrium\\.cli\\.secret import"
---

<objective>
Create CLI secret commands for set, list, and remove operations.

Purpose: Enable users to manage secrets via `clm secret set`, `clm secret list`, and `clm secret remove` commands (SEC-01, SEC-02, SEC-03), with masked input and confirmation prompts following established CLI patterns.

Output:
- `src/clawrium/cli/secret.py` — CLI commands using Typer
- `src/clawrium/cli/main.py` — Updated to register secret subcommand
- `tests/test_cli_secret.py` — CLI tests
</objective>

<execution_context>
@/home/devashish/workspace/ric03uec/clawrium/.claude/get-shit-done/workflows/execute-plan.md
@/home/devashish/workspace/ric03uec/clawrium/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/05-secrets-management/05-CONTEXT.md
@.planning/phases/05-secrets-management/05-01-SUMMARY.md

<interfaces>
<!-- Key types and contracts from Plan 01 that this plan depends on -->

From src/clawrium/core/secrets.py (created in Plan 01):
```python
SECRETS_FILE = "secrets.json"

class SecretEntry(TypedDict):
    key: str
    value: str
    created_at: str
    updated_at: str
    description: str

def get_secret(key: str) -> SecretEntry | None:
    """Return secret entry or None."""

def set_secret(key: str, value: str, description: str = "") -> bool:
    """Set/update secret. Return True if created new, False if updated."""

def remove_secret(key: str) -> bool:
    """Remove by key. Return True if found, False otherwise."""

def list_secrets() -> list[str]:
    """Return sorted list of secret keys."""

def load_secrets() -> dict[str, SecretEntry]:
    """Load all secrets (for metadata display)."""
```

From src/clawrium/core/registry.py (extended in Plan 01):
```python
class SecretDefinition(TypedDict):
    key: str
    description: str

def get_required_secrets(claw_name: str) -> list[SecretDefinition]:
    """Get list of required secrets for a claw."""

def list_claws() -> list[str]:
    """List all available claw types in the registry."""
```

From src/clawrium/core/hosts.py:
```python
def load_hosts() -> list[dict]:
    """Load hosts from JSON file."""
```

From src/clawrium/cli/host.py (patterns to replicate):
```python
import typer
from rich.console import Console
from rich.table import Table

console = Console()

host_app = typer.Typer(
    name="host",
    help="Manage hosts in your fleet",
    no_args_is_help=True,
)

@host_app.command()
def remove(
    hostname: str = typer.Argument(..., help="Host hostname or alias to remove"),
    force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
) -> None:
    """Remove a host from the fleet."""
    if not force:
        confirmed = typer.confirm(f"Remove host '{display_name}'? This cannot be undone.")
        if not confirmed:
            console.print("Cancelled.")
            raise typer.Exit(code=0)
```

From src/clawrium/cli/main.py:
```python
app.add_typer(host_app, name="host")
app.add_typer(registry_app, name="registry")
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create secret CLI commands</name>
  <files>src/clawrium/cli/secret.py, tests/test_cli_secret.py</files>
  <read_first>
    - src/clawrium/cli/host.py (source of truth for CLI patterns: confirmation, table output, typer.Exit)
    - src/clawrium/cli/registry.py (list command pattern)
    - src/clawrium/core/secrets.py (CRUD functions from Plan 01)
    - src/clawrium/core/registry.py (get_required_secrets, list_claws)
    - tests/test_cli_host.py (test patterns for CLI commands)
    - tests/conftest.py (fixtures)
  </read_first>
  <behavior>
    - Test: `clm secret set KEY` prompts for value, stores secret
    - Test: `clm secret set KEY --description "desc"` stores with description
    - Test: `clm secret set KEY` on existing key shows warning and prompts for overwrite confirmation
    - Test: `clm secret set KEY` with --yes skips overwrite confirmation
    - Test: `clm secret list` shows table with Key, Description, Updated columns (no Value)
    - Test: `clm secret list` shows "Missing Required Secrets" section if any claw has unset required secrets
    - Test: `clm secret remove KEY` prompts for confirmation
    - Test: `clm secret remove KEY --force` skips confirmation
    - Test: `clm secret remove KEY` for non-existent key shows error
  </behavior>
  <action>
Create `src/clawrium/cli/secret.py` with:

1. **Module setup**:
```python
"""Secret management commands for Clawrium."""

import getpass
from typing import Optional

import typer
from rich.console import Console
from rich.table import Table

from clawrium.core.secrets import (
    get_secret,
    set_secret,
    remove_secret,
    list_secrets,
    load_secrets,
    SecretsFileCorruptedError,
)
from clawrium.core.registry import (
    get_required_secrets,
    list_claws,
)
from clawrium.core.hosts import load_hosts

__all__ = ["secret_app"]

console = Console()

secret_app = typer.Typer(
    name="secret",
    help="Manage secrets for claw instances",
    no_args_is_help=True,
)
```

2. **set command** (per D-09, D-10):
```python
@secret_app.command()
def set(
    key: str = typer.Argument(..., help="Secret key name (e.g., OPENAI_API_KEY)"),
    description: Optional[str] = typer.Option(
        None, "--description", "-d", help="Description of the secret"
    ),
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip overwrite confirmation"),
) -> None:
    """Set a secret value.

    Prompts for the value using masked input (not visible on screen).
    """
    # Check if secret already exists (per D-10)
    existing = get_secret(key)
    if existing and not yes:
        console.print(f"[yellow]Secret '{key}' already exists[/yellow]")
        console.print(f"  Description: {existing.get('description') or '-'}")
        console.print(f"  Last updated: {existing.get('updated_at', 'Unknown')}")
        if not typer.confirm("Overwrite this secret?"):
            console.print("Cancelled.")
            raise typer.Exit(code=0)

    # Prompt for value with masked input (per D-09)
    try:
        value = getpass.getpass(prompt=f"Enter value for {key}: ")
    except (KeyboardInterrupt, EOFError):
        console.print("\nCancelled.")
        raise typer.Exit(code=1)

    if not value:
        console.print("[red]Error:[/red] Secret value cannot be empty")
        raise typer.Exit(code=1)

    # Set the secret
    is_new = set_secret(key, value, description or "")

    if is_new:
        console.print(f"[green]Secret '{key}' created.[/green]")
    else:
        console.print(f"[green]Secret '{key}' updated.[/green]")
```

3. **list command** (per D-08, D-11, D-13):
```python
@secret_app.command(name="list")
def list_cmd() -> None:
    """List all stored secrets.

    Shows secret keys and metadata. Values are never displayed.
    Also shows missing required secrets per claw type.
    """
    try:
        secrets = load_secrets()
    except SecretsFileCorruptedError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(code=1)

    # Display stored secrets
    if secrets:
        table = Table(title="Stored Secrets")
        table.add_column("Key", style="cyan")
        table.add_column("Description")
        table.add_column("Updated", style="dim")

        for key in sorted(secrets.keys()):
            entry = secrets[key]
            # Format updated_at as date only for readability
            updated = entry.get("updated_at", "")
            if updated:
                updated = updated.split("T")[0]  # Extract date portion
            table.add_row(
                key,
                entry.get("description") or "-",
                updated,
            )

        console.print(table)
    else:
        console.print("No secrets stored. Use 'clm secret set KEY' to add a secret.")

    # Check for missing required secrets per claw (per D-08)
    stored_keys = set(secrets.keys())
    missing_by_claw: dict[str, list[dict]] = {}

    for claw_name in list_claws():
        required = get_required_secrets(claw_name)
        missing = [s for s in required if s["key"] not in stored_keys]
        if missing:
            missing_by_claw[claw_name] = missing

    if missing_by_claw:
        console.print("")  # Blank line separator
        console.print("[yellow]Missing Required Secrets[/yellow]")

        for claw_name, missing_secrets in sorted(missing_by_claw.items()):
            console.print(f"\n  [cyan]{claw_name}[/cyan]")
            for s in missing_secrets:
                console.print(f"    - {s['key']}: {s.get('description', '')}")
```

4. **remove command** (per D-12):
```python
@secret_app.command()
def remove(
    key: str = typer.Argument(..., help="Secret key to remove"),
    force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
) -> None:
    """Remove a secret.

    Prompts for confirmation unless --force is specified.
    """
    # Check if secret exists
    existing = get_secret(key)
    if not existing:
        console.print(f"[red]Error:[/red] Secret '{key}' not found")
        raise typer.Exit(code=1)

    # Confirmation (per D-12, matching host remove pattern)
    if not force:
        confirmed = typer.confirm(f"Remove secret '{key}'? This cannot be undone.")
        if not confirmed:
            console.print("Cancelled.")
            raise typer.Exit(code=0)

    success = remove_secret(key)
    if success:
        console.print(f"[green]Secret '{key}' removed.[/green]")
    else:
        console.print(f"[red]Error:[/red] Failed to remove secret")
        raise typer.Exit(code=1)
```

Create `tests/test_cli_secret.py` with tests for all behaviors using typer.testing.CliRunner and appropriate mocks for getpass.getpass.
  </action>
  <verify>
    <automated>cd /home/devashish/workspace/ric03uec/clawrium && make test PYTEST_ARGS="-x tests/test_cli_secret.py -v"</automated>
  </verify>
  <acceptance_criteria>
    - src/clawrium/cli/secret.py exists and contains `secret_app = typer.Typer(`
    - src/clawrium/cli/secret.py contains `def set(`
    - src/clawrium/cli/secret.py contains `def list_cmd(`
    - src/clawrium/cli/secret.py contains `def remove(`
    - src/clawrium/cli/secret.py contains `getpass.getpass(`
    - src/clawrium/cli/secret.py contains `typer.confirm(`
    - src/clawrium/cli/secret.py contains `from clawrium.core.secrets import`
    - src/clawrium/cli/secret.py contains `Missing Required Secrets`
    - tests/test_cli_secret.py contains `def test_secret_set_`
    - tests/test_cli_secret.py contains `def test_secret_list`
    - tests/test_cli_secret.py contains `def test_secret_remove`
    - `make test PYTEST_ARGS="-x tests/test_cli_secret.py"` exits 0
  </acceptance_criteria>
  <done>
    - secret.py implements set, list, remove commands with proper UX
    - Masked input for secret values (getpass)
    - Confirmation prompts before overwrite and remove
    - Missing required secrets displayed in list output
    - All CLI tests pass
  </done>
</task>

<task type="auto">
  <name>Task 2: Register secret subcommand in main CLI</name>
  <files>src/clawrium/cli/main.py</files>
  <read_first>
    - src/clawrium/cli/main.py (current state, registration pattern)
    - src/clawrium/cli/secret.py (secret_app export from Task 1)
  </read_first>
  <action>
Update `src/clawrium/cli/main.py`:

1. Add import at top with other CLI imports:
```python
from clawrium.cli.secret import secret_app
```

2. Add registration after existing app.add_typer calls:
```python
# Register secret subcommands
app.add_typer(secret_app, name="secret")
```

The final imports section should look like:
```python
from clawrium.cli.init import init as init_command
from clawrium.cli.host import host_app
from clawrium.cli.install import install as install_command
from clawrium.cli.registry import registry_app
from clawrium.cli.secret import secret_app
from clawrium.cli.status import status as status_command
```

The final registrations section should look like:
```python
# Register host subcommands
app.add_typer(host_app, name="host")

# Register registry subcommands
app.add_typer(registry_app, name="registry")

# Register secret subcommands
app.add_typer(secret_app, name="secret")
```
  </action>
  <verify>
    <automated>cd /home/devashish/workspace/ric03uec/clawrium && python -c "from clawrium.cli.main import app; print([c.name for c in app.registered_commands])" && clm secret --help</automated>
  </verify>
  <acceptance_criteria>
    - src/clawrium/cli/main.py contains `from clawrium.cli.secret import secret_app`
    - src/clawrium/cli/main.py contains `app.add_typer(secret_app, name="secret")`
    - `clm secret --help` shows set, list, remove commands
    - `clm --help` shows secret in command list
  </acceptance_criteria>
  <done>
    - secret subcommand registered and accessible via `clm secret`
    - Help text displays correctly
  </done>
</task>

</tasks>

<verification>
After completing all tasks:

1. Verify CLI registration:
```bash
clm --help | grep secret
clm secret --help
```

2. Test full flow (manual verification):
```bash
# Set a secret
clm secret set TEST_KEY --description "Test secret"
# Enter value when prompted

# List secrets
clm secret list
# Should show TEST_KEY with description

# Remove secret
clm secret remove TEST_KEY
# Confirm when prompted
```

3. Run all tests:
```bash
make test
```
</verification>

<success_criteria>
- `clm secret set KEY` prompts for masked input and stores secret
- `clm secret list` shows keys (not values) and missing required secrets
- `clm secret remove KEY` prompts for confirmation and removes
- All tests pass
- No secret values ever displayed in output
</success_criteria>

<output>
After completion, create `.planning/phases/05-secrets-management/05-02-SUMMARY.md`
</output>
