Back to skills
SkillHub ClubShip Full StackFull StackBackend

worklog

Track billable hours for clients. This skill should be used when the user requests to log work hours, record time spent on client projects, view worklog entries, calculate total billable hours, or summarize recent work. Automatically prompts for missing information (client, hours, description) and validates client names against the invoice skill's client database.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
4
Hot score
81
Updated
March 20, 2026
Overall rating
C3.1
Composite score
3.1
Best-practice grade
C67.9

Install command

npx @skill-hub/cli install arlenagreer-claude-configuration-docs-worklog

Repository

arlenagreer/claude_configuration_docs

Skill path: skills/worklog

Track billable hours for clients. This skill should be used when the user requests to log work hours, record time spent on client projects, view worklog entries, calculate total billable hours, or summarize recent work. Automatically prompts for missing information (client, hours, description) and validates client names against the invoice skill's client database.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: arlenagreer.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install worklog into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/arlenagreer/claude_configuration_docs before adding worklog to shared team environments
  • Use worklog for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: worklog
description: Track billable hours for clients. This skill should be used when the user requests to log work hours, record time spent on client projects, view worklog entries, calculate total billable hours, or summarize recent work. Automatically prompts for missing information (client, hours, description) and validates client names against the invoice skill's client database.
---

# Worklog Skill

Track billable hours for clients with automatic client validation and integration with the invoice skill.

## Purpose

This skill helps track time spent on client work by maintaining a detailed worklog with client names, dates, hours worked, and work descriptions. It integrates with the invoice skill by reading client information from `~/.claude/skills/invoice/clients.json` to ensure consistency and enable accurate billing.

## When to Use This Skill

Use this skill when:
- The user requests to log work hours for a client
- The user asks to record time spent on a project
- The user wants to view past worklog entries
- The user needs to calculate total hours worked (overall or by client/date range)
- The user asks to summarize work done in the current session for logging
- The user mentions tracking billable hours or time tracking

## How to Use This Skill

### Core Workflow

When the user requests to log work:

1. **Determine the current date** - ALWAYS check the system clock using `date +%Y-%m-%d` to get the current date. Never assume the date based on training cutoff.

2. **Identify required information**:
   - **Client name** - If not provided, list available clients using the worklog manager and ask the user to specify
   - **Hours worked** - If not provided, ask the user to confirm the number of hours
   - **Work description** - If not provided, ask the user for a description
   - **Date** - Use current date unless the user specifies a different date for retroactive entry

3. **Validate client name** - The script automatically validates against the invoice skill's client list. Valid client names are:
   - "American Laboratory Trading" (aliases: "ALT", "alt")
   - "Empirico"
   - "Versa Computing"

4. **Create work description**:
   - If user provides a description, use it as-is
   - If user asks to summarize current session's work, analyze the conversation and recent code changes to create a brief but detailed summary
   - Description should be **one paragraph typically**, but can be several paragraphs if needed to accurately describe the work
   - Focus on what was accomplished, problems solved, features implemented, or issues addressed
   - Be specific about technical details when relevant

5. **Add the entry** using the worklog manager script

### Available Operations

#### List Available Clients

```bash
python3 scripts/worklog_manager.py clients
```

This displays all clients from the invoice skill with their billing type and hourly rate (if applicable).

#### Add Worklog Entry

```bash
python3 scripts/worklog_manager.py add \
  --client "Client Name" \
  --hours 2.5 \
  --description "Work description here" \
  --date 2025-11-01  # Optional, defaults to today
```

**Important**:
- Client name must exactly match a name from the invoice skill (or use a supported alias)
- Hours must be a positive number (can include decimals like 2.5)
- Date format must be YYYY-MM-DD
- If date is omitted, the current date is used automatically

#### Client Aliases

For convenience, the worklog skill supports client name aliases:

- **"ALT"** or **"alt"** → "American Laboratory Trading"

You can use these aliases in any command that accepts a client name:

```bash
# These are equivalent:
python3 scripts/worklog_manager.py add --client "ALT" --hours 2 --description "Work done"
python3 scripts/worklog_manager.py add --client "American Laboratory Trading" --hours 2 --description "Work done"

# Aliases also work for filtering and totals:
python3 scripts/worklog_manager.py list --client "ALT"
python3 scripts/worklog_manager.py total --client "alt"
```

All entries are stored using the canonical client name ("American Laboratory Trading"), so aliases are purely a convenience for input.

#### List Worklog Entries

```bash
# List all entries
python3 scripts/worklog_manager.py list

# Filter by client
python3 scripts/worklog_manager.py list --client "American Laboratory Trading"

# Filter by date range
python3 scripts/worklog_manager.py list --start-date 2025-10-01 --end-date 2025-10-31

# Get JSON output for processing
python3 scripts/worklog_manager.py list --format json
```

#### Calculate Total Hours

```bash
# Total hours for all clients
python3 scripts/worklog_manager.py total

# Total for specific client
python3 scripts/worklog_manager.py total --client "Empirico"

# Total for date range
python3 scripts/worklog_manager.py total --start-date 2025-10-01 --end-date 2025-10-31
```

For hourly clients, this also calculates the billable amount using the rate from the invoice skill.

#### Delete Entry

```bash
# First list entries to see indices
python3 scripts/worklog_manager.py list

# Delete entry at index (0-based)
python3 scripts/worklog_manager.py delete 0
```

### Example Interactions

**Example 1: User provides all information**
```
User: "Log 3 hours for American Laboratory Trading - implemented user authentication system with JWT tokens"

Action: Add entry with provided information using current date
```

**Example 2: User asks to log work without details**
```
User: "Log my work hours"

Claude: "I'll help you log your work hours. Which client is this for? Available clients:
- American Laboratory Trading ($100/hr)
- Empirico (subscription)
- Versa Computing ($100/hr)"

User: "American Laboratory Trading"

Claude: "How many hours did you work?"

User: "2.5 hours"

Claude: "What work did you complete?"

User: "Fixed the login bug and updated the database schema"

Action: Add entry with gathered information
```

**Example 3: User asks to summarize session work**
```
User: "Log my work for Versa Computing - summarize what we did today"

Claude: [Analyzes conversation and recent changes, creates summary like:]
"During this session, implemented a new worklog tracking system that integrates with the existing invoice skill. Created a Python script for managing time entries with client validation, date handling, and reporting features. The system maintains worklog data in JSON format and reads client information from the invoice skill to ensure consistency. Added functionality for adding entries, listing work by client and date range, calculating total hours, and deleting entries."

Action: Ask user to confirm hours, then add entry with generated summary
```

**Example 4: Retroactive entry**
```
User: "Add a worklog entry for yesterday - 4 hours on Empirico, worked on the subscription billing integration"

Action: Calculate yesterday's date using system clock, add entry with that date
```

### Data Files

- **Worklog data**: Stored in `~/.claude/skills/worklog/worklog.json`
- **Client data**: Read from `~/.claude/skills/invoice/clients.json` (read-only)

The worklog skill never modifies the invoice skill's client data - it only reads it for validation and billing rate information.

### Integration with Invoice Skill

This skill integrates with the invoice skill by:
1. Reading the client list to validate client names
2. Using client hourly rates to calculate billable amounts in reports
3. Ensuring client names are consistent between worklog entries and invoices

When creating invoices, reference worklog entries to ensure accurate billing.

### Error Handling

- If an invalid client name is provided, the script will list valid clients and prompt for correction
- If hours are zero or negative, an error will be shown
- If the date format is incorrect, an error will be shown with the expected format
- If the invoice skill's clients.json file is not found, an error will be shown

### Best Practices

1. **Get current date first** - Always run `date +%Y-%m-%d` before logging entries to ensure accuracy
2. **Prompt for confirmation** - When hours are not specified, ask the user to confirm
3. **Create detailed descriptions** - When summarizing work, be specific about accomplishments
4. **Validate client names** - Always use exact client names from the invoice skill
5. **Use consistent formatting** - Keep descriptions professional and focused on deliverables


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/worklog_manager.py

```python
#!/usr/bin/env python3
"""
Worklog Manager - Track billable hours for clients
"""

import json
import os
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Dict

# Path to worklog data file
WORKLOG_FILE = Path(__file__).parent.parent / "worklog.json"
INVOICE_CLIENTS_FILE = Path.home() / ".claude/skills/invoice/clients.json"

# Client name aliases/synonyms
CLIENT_ALIASES = {
    "ALT": "American Laboratory Trading",
    "alt": "American Laboratory Trading",
}


def load_worklog() -> Dict:
    """Load worklog data from JSON file"""
    if not WORKLOG_FILE.exists():
        return {"entries": []}

    with open(WORKLOG_FILE, 'r') as f:
        return json.load(f)


def save_worklog(data: Dict) -> None:
    """Save worklog data to JSON file"""
    with open(WORKLOG_FILE, 'w') as f:
        json.dump(data, f, indent=2)


def load_invoice_clients() -> List[Dict]:
    """Load client list from invoice skill"""
    if not INVOICE_CLIENTS_FILE.exists():
        return []

    with open(INVOICE_CLIENTS_FILE, 'r') as f:
        data = json.load(f)
        return data.get("clients", [])


def get_client_names() -> List[str]:
    """Get list of valid client names from invoice skill"""
    clients = load_invoice_clients()
    return [client["name"] for client in clients]


def resolve_client_alias(client_name: str) -> str:
    """
    Resolve a client alias to the actual client name

    Args:
        client_name: Client name or alias

    Returns:
        The actual client name (or the input if no alias match)
    """
    return CLIENT_ALIASES.get(client_name, client_name)


def validate_client(client_name: str) -> bool:
    """
    Check if client name exists in invoice skill
    Supports both actual names and aliases
    """
    # Resolve alias first
    resolved_name = resolve_client_alias(client_name)
    valid_clients = get_client_names()
    return resolved_name in valid_clients


def get_client_rate(client_name: str) -> Optional[float]:
    """
    Get hourly rate for a client (if hourly billing)
    Supports both actual names and aliases
    """
    # Resolve alias first
    resolved_name = resolve_client_alias(client_name)
    clients = load_invoice_clients()
    for client in clients:
        if client["name"] == resolved_name:
            if client.get("invoice_type") == "hourly":
                return client.get("hourly_rate")
    return None


def add_entry(
    client_name: str,
    hours: float,
    description: str,
    date: Optional[str] = None
) -> Dict:
    """
    Add a new worklog entry

    Args:
        client_name: Name of the client (must match invoice skill) or alias
        hours: Number of hours worked
        description: Description of work performed
        date: Date of work (YYYY-MM-DD format), defaults to today

    Returns:
        The created entry dictionary
    """
    # Resolve alias to actual client name
    resolved_client_name = resolve_client_alias(client_name)

    # Validate client exists
    if not validate_client(client_name):
        valid_clients = get_client_names()
        raise ValueError(
            f"Client '{client_name}' not found in invoice skill. "
            f"Valid clients: {', '.join(valid_clients)}"
        )

    # Validate hours
    if hours <= 0:
        raise ValueError("Hours must be greater than 0")

    # Use today's date if not specified
    if date is None:
        date = datetime.now().strftime("%Y-%m-%d")

    # Validate date format
    try:
        datetime.strptime(date, "%Y-%m-%d")
    except ValueError:
        raise ValueError("Date must be in YYYY-MM-DD format")

    # Create entry (using resolved/canonical client name)
    entry = {
        "client": resolved_client_name,
        "date": date,
        "hours": hours,
        "description": description,
        "hourly_rate": get_client_rate(resolved_client_name),
        "created_at": datetime.now().isoformat()
    }

    # Load, append, save
    worklog = load_worklog()
    worklog["entries"].append(entry)
    save_worklog(worklog)

    return entry


def list_entries(
    client_name: Optional[str] = None,
    start_date: Optional[str] = None,
    end_date: Optional[str] = None
) -> List[Dict]:
    """
    List worklog entries with optional filters

    Args:
        client_name: Filter by client name or alias
        start_date: Filter entries on or after this date (YYYY-MM-DD)
        end_date: Filter entries on or before this date (YYYY-MM-DD)

    Returns:
        List of matching entries
    """
    worklog = load_worklog()
    entries = worklog.get("entries", [])

    # Apply filters
    if client_name:
        # Resolve alias before filtering
        resolved_name = resolve_client_alias(client_name)
        entries = [e for e in entries if e["client"] == resolved_name]

    if start_date:
        entries = [e for e in entries if e["date"] >= start_date]

    if end_date:
        entries = [e for e in entries if e["date"] <= end_date]

    # Sort by date (most recent first)
    entries.sort(key=lambda e: e["date"], reverse=True)

    return entries


def get_total_hours(
    client_name: Optional[str] = None,
    start_date: Optional[str] = None,
    end_date: Optional[str] = None
) -> Dict[str, float]:
    """
    Calculate total hours worked

    Args:
        client_name: Filter by client name
        start_date: Filter entries on or after this date (YYYY-MM-DD)
        end_date: Filter entries on or before this date (YYYY-MM-DD)

    Returns:
        Dictionary with total hours by client
    """
    entries = list_entries(client_name, start_date, end_date)

    totals = {}
    for entry in entries:
        client = entry["client"]
        hours = entry["hours"]
        totals[client] = totals.get(client, 0) + hours

    return totals


def delete_entry(index: int) -> bool:
    """
    Delete an entry by its index in the list

    Args:
        index: Zero-based index of entry to delete

    Returns:
        True if deleted, False if index out of range
    """
    worklog = load_worklog()
    entries = worklog.get("entries", [])

    if 0 <= index < len(entries):
        deleted = entries.pop(index)
        save_worklog(worklog)
        return True

    return False


def main():
    """CLI interface for worklog manager"""
    import argparse

    parser = argparse.ArgumentParser(description="Manage billable hours worklog")
    subparsers = parser.add_subparsers(dest="command", help="Command to execute")

    # Add entry command
    add_parser = subparsers.add_parser("add", help="Add a new worklog entry")
    add_parser.add_argument("--client", required=True, help="Client name")
    add_parser.add_argument("--hours", type=float, required=True, help="Hours worked")
    add_parser.add_argument("--description", required=True, help="Work description")
    add_parser.add_argument("--date", help="Date (YYYY-MM-DD), defaults to today")

    # List entries command
    list_parser = subparsers.add_parser("list", help="List worklog entries")
    list_parser.add_argument("--client", help="Filter by client name")
    list_parser.add_argument("--start-date", help="Start date (YYYY-MM-DD)")
    list_parser.add_argument("--end-date", help="End date (YYYY-MM-DD)")
    list_parser.add_argument("--format", choices=["json", "table"], default="table")

    # Total hours command
    total_parser = subparsers.add_parser("total", help="Calculate total hours")
    total_parser.add_argument("--client", help="Filter by client name")
    total_parser.add_argument("--start-date", help="Start date (YYYY-MM-DD)")
    total_parser.add_argument("--end-date", help="End date (YYYY-MM-DD)")

    # List clients command
    subparsers.add_parser("clients", help="List available clients")

    # Delete entry command
    delete_parser = subparsers.add_parser("delete", help="Delete an entry")
    delete_parser.add_argument("index", type=int, help="Entry index to delete")

    args = parser.parse_args()

    if args.command == "add":
        try:
            entry = add_entry(
                args.client,
                args.hours,
                args.description,
                args.date
            )
            print(f"✅ Added entry: {entry['hours']} hours for {entry['client']} on {entry['date']}")
        except ValueError as e:
            print(f"❌ Error: {e}")
            return 1

    elif args.command == "list":
        entries = list_entries(args.client, args.start_date, args.end_date)

        if args.format == "json":
            print(json.dumps(entries, indent=2))
        else:
            if not entries:
                print("No entries found.")
            else:
                print(f"\n{'Date':<12} {'Client':<25} {'Hours':<8} {'Description'}")
                print("-" * 80)
                for entry in entries:
                    desc = entry["description"][:40] + "..." if len(entry["description"]) > 40 else entry["description"]
                    print(f"{entry['date']:<12} {entry['client']:<25} {entry['hours']:<8.2f} {desc}")
                print()

    elif args.command == "total":
        totals = get_total_hours(args.client, args.start_date, args.end_date)

        if not totals:
            print("No entries found.")
        else:
            print("\nTotal Hours by Client:")
            print("-" * 40)
            for client, hours in sorted(totals.items()):
                rate = get_client_rate(client)
                if rate:
                    amount = hours * rate
                    print(f"{client:<30} {hours:>6.2f} hrs (${amount:,.2f})")
                else:
                    print(f"{client:<30} {hours:>6.2f} hrs")
            print()

    elif args.command == "clients":
        clients = load_invoice_clients()
        print("\nAvailable Clients:")
        print("-" * 60)
        for client in clients:
            rate_info = f"${client['hourly_rate']}/hr" if client.get('invoice_type') == 'hourly' else client.get('invoice_type', 'N/A')
            print(f"{client['name']:<35} ({rate_info})")
        print()

    elif args.command == "delete":
        if delete_entry(args.index):
            print(f"✅ Deleted entry at index {args.index}")
        else:
            print(f"❌ Error: Invalid index {args.index}")
            return 1

    else:
        parser.print_help()
        return 1

    return 0


if __name__ == "__main__":
    exit(main())

```

worklog | SkillHub