Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Excel Report

ASecurity

Create an auditable Markdown report for any uploaded Microsoft Excel (.xlsx) or CSV files in an Openrind Shell sandbox.

3 stars
0 votes
0 copies
0 views
Added 9/26/2026
toolspythongoshellbashgitapidatabasesecurity

Works with

api

Security Analysis

A92/100
mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned 10/4/2026

$npx -y skills add openrind/openrind-shell --skill excel-report --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Excel Report?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Excel Report
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/openrind-excel-report/badge)](https://www.skillsdirectory.com/skills/openrind-excel-report)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
name: excel-report
description: Create an auditable Markdown report for any uploaded Microsoft Excel (.xlsx) or CSV files in an Openrind Shell sandbox.
disable-model-invocation: false
user-invocable: true
allowed-tools: Read, Bash, Glob, Write
argument-hint: [optional: path to .xlsx or .csv file(s)]
---

# Excel & CSV Reporting

**CRITICAL INSTRUCTION FOR CLAUDE**:
- This skill does **NOT** spawn background processes or background task IDs. There is **NO** task ID (never call `Task Output` or wait for `skill_run`).
- You (Claude) must directly and immediately execute the `Bash` command in Step 2 to generate the parsed data and then write the final report.
- Never claim data was analyzed until the parsing command succeeds.

## Core Rules

- **Zero External Dependencies**: Standard `python3` (built-in `zipfile`, `xml.etree.ElementTree`, `csv`, `math`, `statistics`, `json`, `datetime`) is used directly.
- **Never probe or search for tools**: Do NOT execute `which libreoffice`, `which csvkit`, `which unzip`, or `apt list`.
- **Never run network package installs**: Do NOT run `pip install`, `uv pip install`, or attempt downloading external packages. The built-in Python script parses `.xlsx` OpenXML and `.csv` natively, offline, and in milliseconds.
- **Bounded input**: Reject files over 10 MiB, ZIP expansion over 32 MiB, members over 8 MiB, more than 128 members, 10,000 rows, 256 columns, or 250,000 dense cells per sheet. Do not retry rejected files without splitting them first.
- **Privacy & Security Gate**: Strictly prohibit exposing secrets, credentials (tokens, passwords, API keys, private keys, database URLs), and unnecessary personal data (PII, SSNs, credit card numbers). Redact sensitive columns and values (`[REDACTED]`). Row-level samples must be redacted, and do not dump raw row-level data or full reports in chat when row-level or sensitive records are present.

## Step 1: Locate Target Files

Determine the inbox directory and find the target file(s):

```bash
if mountpoint -q /sandbox/work 2>/dev/null; then
  INBOX=/sandbox/work/inbox
elif [ -d "/sandbox/inbox" ]; then
  INBOX=/sandbox/inbox
else
  INBOX="."
fi
mkdir -p "$INBOX"
```

If specific file path(s) were given in the user prompt or skill argument, use those.
Otherwise, discover all workbooks dynamically:

```bash
find "$INBOX" /sandbox . -maxdepth 3 -type f \( -iname '*.xlsx' -o -iname '*.csv' \) 2>/dev/null | sort -u
```

## Step 2: Run The Self-Contained Parser

Create `/tmp/openrind-excel-parse.py` and run it against the target file(s) with `python3`:

```bash
cat << 'EOF' > /tmp/openrind-excel-parse.py
import sys, os, zipfile, csv, json, re, math, statistics, datetime, io
from pathlib import Path
import xml.etree.ElementTree as ET
from collections import Counter

MAX_FILE = 10 * 1024 * 1024
MAX_ROWS, MAX_COLS, MAX_CELLS = 10000, 256, 250000
csv.field_size_limit(65536)

def check_file(path):
    if Path(path).stat().st_size > MAX_FILE:
        raise ValueError('Input exceeds the 10 MiB safety limit')

def check_shape(rows, cols):
    if rows > MAX_ROWS or cols > MAX_COLS or rows * cols > MAX_CELLS:
        raise ValueError('Sheet exceeds safe row, column, or cell limits')

def col_letter_to_index(col_str):
    idx = 0
    for char in col_str.upper():
        if 'A' <= char <= 'Z':
            idx = idx * 26 + (ord(char) - ord('A') + 1)
    return idx - 1

def index_to_col_letter(idx):
    res = []
    idx += 1
    while idx > 0:
        idx, rem = divmod(idx - 1, 26)
        res.append(chr(ord('A') + rem))
    return ''.join(reversed(res))

def parse_cell_ref(cell_ref):
    m = re.match(r'([A-Za-z]+)([0-9]+)', cell_ref)
    if m:
        return col_letter_to_index(m.group(1)), int(m.group(2))
    return None, None

def is_date_format_code(code):
    if not code:
        return False
    cleaned = re.sub(r'"[^"]*"', '', code)
    cleaned = re.sub(r'\[(?!\s*[hmsHMS]\s*\])[^\]]*\]', '', cleaned)
    cleaned = re.sub(r'\\.', '', cleaned)
    cleaned = re.sub(r'[_\*].', '', cleaned)
    cl = cleaned.lower()
    if re.search(r'[ydhs]|am/pm|a/p', cl):
        return True
    if re.search(r'(^|[^a-z0-9])m+($|[^a-z0-9])', cl):
        return True
    return False

def format_excel_date(serial, is_1904=False):
    if is_1904:
        base = datetime.datetime(1904, 1, 1)
        dt = base + datetime.timedelta(days=serial)
    else:
        base = datetime.datetime(1899, 12, 31) if serial < 60 else datetime.datetime(1899, 12, 30)
        dt = base + datetime.timedelta(days=serial)
    if dt.microsecond >= 500000:
        dt += datetime.timedelta(seconds=1)
    if serial < 1 and not is_1904:
        return dt.strftime('%H:%M:%S')
    if dt.hour == 0 and dt.minute == 0 and dt.second == 0:
        return dt.strftime('%Y-%m-%d')
    return dt.strftime('%Y-%m-%d %H:%M:%S')

def read_xlsx(file_path):
    check_file(file_path)
    sheets_data = {}
    with zipfile.ZipFile(file_path, 'r') as z:
        members = z.infolist()
        if (len(members) > 128 or sum(m.file_size for m in members) > 32 * 1024 * 1024
                or any(m.file_size > 8 * 1024 * 1024 or m.file_size > max(1, m.compress_size) * 200 for m in members)):
            raise ValueError('Workbook exceeds ZIP expansion safety limits')
        is_1904 = False
        if 'xl/workbook.xml' in z.namelist():
            wb_tree = ET.fromstring(z.read('xl/workbook.xml'))
            wb_pr = wb_tree.find('.//{*}workbookPr')
            if wb_pr is not None and wb_pr.get('date1904') in ('1', 'true', 'True'):
                is_1904 = True

        custom_num_fmts = {}
        cell_xfs_num_fmt_ids = []
        if 'xl/styles.xml' in z.namelist():
            styles_tree = ET.fromstring(z.read('xl/styles.xml'))
            for num_fmt in styles_tree.findall('.//{*}numFmt'):
                fmt_id_str = num_fmt.get('numFmtId')
                fmt_code = num_fmt.get('formatCode')
                if fmt_id_str is not None and fmt_code is not None:
                    try:
                        custom_num_fmts[int(fmt_id_str)] = fmt_code
                    except ValueError:
                        pass
            for xf in styles_tree.findall('.//{*}cellXfs/{*}xf'):
                num_fmt_id_str = xf.get('numFmtId', '0')
                try:
                    cell_xfs_num_fmt_ids.append(int(num_fmt_id_str))
                except ValueError:
                    cell_xfs_num_fmt_ids.append(0)

        builtin_date_fmt_ids = {
            14, 15, 16, 17, 18, 19, 20, 21, 22,
            27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
            45, 46, 47, 50, 51, 52, 53, 54, 55, 56, 57, 58
        }
        date_style_indices = set()
        for idx, fmt_id in enumerate(cell_xfs_num_fmt_ids):
            if fmt_id in builtin_date_fmt_ids:
                date_style_indices.add(idx)
            elif fmt_id in custom_num_fmts and is_date_format_code(custom_num_fmts[fmt_id]):
                date_style_indices.add(idx)

        shared_strings = []
        if 'xl/sharedStrings.xml' in z.namelist():
            tree = ET.fromstring(z.read('xl/sharedStrings.xml'))
            for si in tree.findall('.//{*}si'):
                texts = [t.text for t in si.findall('.//{*}t') if t.text is not None]
                shared_strings.append(''.join(texts))

        rel_map = {}
        if 'xl/_rels/workbook.xml.rels' in z.namelist():
            rels_tree = ET.fromstring(z.read('xl/_rels/workbook.xml.rels'))
            for rel in rels_tree.findall('.//{*}Relationship'):
                r_id = rel.get('Id')
                target = rel.get('Target', '')
                if target.startswith('/'):
                    target = target[1:]
                if not target.startswith('xl/'):
                    target = 'xl/' + target
                rel_map[r_id] = target

        sheets = []
        if 'xl/workbook.xml' in z.namelist():
            wb_tree = ET.fromstring(z.read('xl/workbook.xml'))
            for idx, sheet_el in enumerate(wb_tree.findall('.//{*}sheet')):
                name = sheet_el.get('name') or f'Sheet{idx+1}'
                r_id = (sheet_el.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id')
                        or sheet_el.get('r:id') or sheet_el.get('id'))
                target = rel_map.get(r_id, f'xl/worksheets/sheet{idx+1}.xml')
                sheets.append({'name': name, 'path': target})

        if not sheets:
            for name in z.namelist():
                if name.startswith('xl/worksheets/sheet') and name.endswith('.xml'):
                    sheets.append({'name': Path(name).stem, 'path': name})

        for s in sheets:
            path = s['path']
            if path not in z.namelist():
                alt = 'xl/' + path if not path.startswith('xl/') else path[3:]
                if alt in z.namelist():
                    path = alt
                else:
                    continue

            ws_tree = ET.fromstring(z.read(path))
            rows_data = []
            max_col = 0

            for row_el in ws_tree.findall('.//{*}row'):
                row_dict = {}
                for c_el in row_el.findall('.//{*}c'):
                    ref = c_el.get('r')
                    t = c_el.get('t', 'n')
                    style_attr = c_el.get('s')
                    style_idx = int(style_attr) if style_attr and style_attr.isdigit() else 0
                    is_date_cell = style_idx in date_style_indices
                    val = None

                    if t == 's':
                        v_el = c_el.find('.//{*}v')
                        if v_el is not None and v_el.text:
                            try:
                                s_idx = int(v_el.text)
                                if s_idx < len(shared_strings):
                                    val = shared_strings[s_idx]
                            except ValueError:
                                val = v_el.text
                    elif t == 'inlineStr':
                        texts = [t_el.text for t_el in c_el.findall('.//{*}t') if t_el.text is not None]
                        val = ''.join(texts) if texts else None
                    elif t == 'b':
                        v_el = c_el.find('.//{*}v')
                        val = (v_el.text == '1') if v_el is not None and v_el.text else False
                    elif t == 'd':
                        v_el = c_el.find('.//{*}v')
                        val = v_el.text if v_el is not None else None
                    elif t in ('str', 'e'):
                        v_el = c_el.find('.//{*}v')
                        val = v_el.text if v_el is not None else None
                    else:
                        v_el = c_el.find('.//{*}v')
                        if v_el is not None and v_el.text:
                            raw = v_el.text
                            try:
                                f_val = float(raw)
                                if is_date_cell:
                                    try:
                                        val = format_excel_date(f_val, is_1904)
                                    except Exception:
                                        val = int(f_val) if f_val.is_integer() else f_val
                                else:
                                    val = int(f_val) if f_val.is_integer() else f_val
                            except ValueError:
                                val = raw

                    col_idx = None
                    if ref:
                        col_idx, _ = parse_cell_ref(ref)
                    if col_idx is not None:
                        check_shape(len(rows_data) + 1, col_idx + 1)
                        row_dict[col_idx] = val
                        if col_idx + 1 > max_col:
                            max_col = col_idx + 1
                rows_data.append(row_dict)
                check_shape(len(rows_data), max_col)

            matrix = []
            for r in rows_data:
                row = [r.get(c, None) for c in range(max_col)]
                matrix.append(row)

            sheets_data[s['name']] = matrix

    return sheets_data

def read_csv(file_path):
    check_file(file_path)
    encodings = ['utf-8-sig', 'utf-8', 'latin-1', 'cp1252']
    raw_bytes = Path(file_path).read_bytes()
    text = None
    for enc in encodings:
        try:
            text = raw_bytes.decode(enc)
            break
        except UnicodeDecodeError:
            continue
    if text is None:
        text = raw_bytes.decode('utf-8', errors='replace')
    try:
        dialect = csv.Sniffer().sniff(text[:4096])
        delimiter = dialect.delimiter
    except Exception:
        delimiter = ','
    reader = csv.reader(io.StringIO(text), delimiter=delimiter)
    matrix = []
    for row in reader:
        check_shape(len(matrix) + 1, len(row))
        converted = []
        for cell in row:
            val = cell.strip()
            converted.append(val if val != '' else None)
        matrix.append(converted)
    return {'CSV Data': matrix}

def is_sensitive_name(name):
    n = str(name).lower()
    patterns = ['password', 'secret', 'token', 'key', 'ssn', 'auth', 'credential', 'credit_card', 'card_number', 'api_key', 'private', 'bearer', 'cvv', 'pin']
    return any(p in n for p in patterns)

def analyze_matrix(matrix):
    if not matrix:
        return {'total_rows': 0, 'total_cols': 0, 'duplicate_rows': 0, 'headers': [], 'sample': [], 'columns': {}}
    raw_headers = [str(c) if c is not None and str(c).strip() != '' else f'Column_{i+1}' for i, c in enumerate(matrix[0])]
    data_rows = matrix[1:]
    total_rows = len(data_rows)
    total_cols = len(raw_headers)
    row_tuples = [tuple(r) for r in data_rows]
    dup_count = len(row_tuples) - len(set(row_tuples)) if total_rows > 0 else 0

    seen_header_counts = {}
    unique_keys = []
    for h in raw_headers:
        if h in seen_header_counts:
            seen_header_counts[h] += 1
            unique_keys.append(f"{h}_{seen_header_counts[h]}")
        else:
            seen_header_counts[h] = 1
            unique_keys.append(h)

    # Default-deny value disclosure: arbitrary notes, identifiers, and numeric
    # account/card values cannot be reliably classified using header names.
    sensitive_cols = set(range(len(raw_headers)))
    redacted_sample = []
    for r in data_rows[:5]:
        redacted_sample.append([('[REDACTED]' if c_idx in sensitive_cols else (r[c_idx] if c_idx < len(r) else None)) for c_idx in range(len(raw_headers))])
    columns_info = {}
    for col_idx, (col_name, col_key) in enumerate(zip(raw_headers, unique_keys)):
        values = [r[col_idx] if col_idx < len(r) else None for r in data_rows]
        non_null_values = [v for v in values if v is not None and v != '']
        null_count = total_rows - len(non_null_values)
        null_pct = (null_count / total_rows * 100) if total_rows > 0 else 0.0
        unique_count = len(set(non_null_values))
        numeric_vals = []
        for v in non_null_values:
            if isinstance(v, (int, float)) and not isinstance(v, bool):
                numeric_vals.append(float(v))
            elif isinstance(v, str):
                try:
                    numeric_vals.append(float(v.strip().replace(',', '')))
                except ValueError:
                    pass
        is_numeric = len(numeric_vals) == len(non_null_values) and len(non_null_values) > 0
        col_info = {
            'header': col_name,
            'col_index': col_idx,
            'col_letter': index_to_col_letter(col_idx),
            'dtype': 'numeric' if is_numeric else 'text',
            'missing_count': null_count,
            'missing_pct': round(null_pct, 2),
            'unique_count': unique_count
        }
        if col_idx in sensitive_cols:
            col_info['values_withheld'] = True
        elif is_numeric and numeric_vals:
            numeric_vals.sort()
            n = len(numeric_vals)
            mean_val = statistics.mean(numeric_vals)
            std_val = statistics.stdev(numeric_vals) if n > 1 else 0.0
            def percentile(s, p):
                idx = (len(s) - 1) * p
                l, u = int(math.floor(idx)), int(math.ceil(idx))
                return s[l] if l == u else s[l] * (u - idx) + s[u] * (idx - l)
            q1, q3 = percentile(numeric_vals, 0.25), percentile(numeric_vals, 0.75)
            iqr = q3 - q1
            outliers = [v for v in numeric_vals if v < (q1 - 1.5*iqr) or v > (q3 + 1.5*iqr)]
            col_info['stats'] = {
                'count': n, 'sum': round(sum(numeric_vals), 2), 'mean': round(mean_val, 2), 'std': round(std_val, 2),
                'min': round(numeric_vals[0], 2), 'q1': round(q1, 2), 'median': round(statistics.median(numeric_vals), 2),
                'q3': round(q3, 2), 'max': round(numeric_vals[-1], 2), 'iqr': round(iqr, 2), 'outlier_count': len(outliers)
            }
        else:
            if col_idx in sensitive_cols:
                col_info['top_values'] = [{'value': '[REDACTED]', 'count': len(non_null_values), 'pct': 100.0}]
            else:
                counts = Counter([str(v) for v in non_null_values]).most_common(5)
                col_info['top_values'] = [{'value': k, 'count': c, 'pct': round(c/len(non_null_values)*100, 1) if non_null_values else 0} for k, c in counts]
        columns_info[col_key] = col_info
    return {'total_rows': total_rows, 'total_cols': total_cols, 'duplicate_rows': dup_count, 'headers': raw_headers, 'sample': redacted_sample, 'columns': columns_info}

def process_file(p):
    path = Path(p)
    if not path.exists(): return None
    sfx = path.suffix.lower()
    if sfx in ('.xlsx', '.xlsm', '.xltx'):
        sheets = read_xlsx(path); fmt = 'Excel Workbook'
    elif sfx in ('.csv', '.tsv', '.txt'):
        sheets = read_csv(path); fmt = 'CSV'
    else: return None
    return {'filename': path.name, 'path': str(path.absolute()), 'size_bytes': path.stat().st_size, 'format': fmt, 'sheets': {k: {'stats': analyze_matrix(v)} for k, v in sheets.items()}}

target_files = sys.argv[1:]
results = []
for f in target_files:
    res = process_file(f)
    if res is not None:
        results.append(res)
print(json.dumps(results, indent=2))
EOF
```

Run against the target file(s):

```bash
python3 /tmp/openrind-excel-parse.py "<path_to_file1>" ["<path_to_file2>" ...] > /tmp/parsed-excel.json
```

## Step 3: Write The Report Contract

Read `/tmp/parsed-excel.json` and generate `$INBOX/analysis-report.md` with:

1. **Executive Summary** — concise, factual overview of the files analyzed and critical findings.
2. **Data Scope** — table showing file names, worksheet names, dimensions (rows × columns), and duplicate rows.
3. **Key Metrics And Statistics** — tables with counts, means, medians, std dev, min, max, IQR outliers, and missing counts.
4. **Detailed Findings** — column diagnostics, data distributions, categorical frequencies, and redacted/opt-in data previews.
5. **Data-Quality Risks** — explicit itemization of missing values, duplicate records, and outliers tied to specific workbooks, sheets, and columns.
6. **Next Steps** — actionable recommendations and investigative questions for further analysis.

Tie each finding to a named workbook, sheet, and column. Distinguish observed facts
from inference. Ensure secrets, credentials, and unnecessary personal data are never printed in plaintext; redact sensitive columns and values. After writing the file, tell the user its exact path. Present an executive summary and high-level findings in chat; do not require or dump the full report in chat when it contains row-level data or sensitive details.

Attribution

openrindopenrind
View sourceSee grades on GitHubMore from openrind →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

ucoz-landing-skill

Create and edit uCoz homepage landing pages via MCP: custom templates, hero sections, lead forms, navigation menus, SEO, and responsive layout. Includes a visual design system (style selection, layout/grid, section recipes, typography/spacing, color tokens, component states, icons, modern CSS/JS, motion, imagery, social proof, copy/voice, accessibility). Uses ucoz-mcp tools for templates, site file uploads, and site modules.

107 votes

Paperclip

Interact with the Paperclip control plane API for task coordination and governance. Use when checking assignments, updating issue status, posting comments, delegating work, managing routines, or calling Paperclip API endpoints.

953191 votes

Pptx

Presentation toolkit (.pptx). Create/edit slides, layouts, content, speaker notes, comments, for programmatic presentation creation and modification.

471861 votes

Daw Music

Digital Audio Workstation usage, music composition, interactive music systems, and game audio implementation for immersive soundscapes.

761 votes

Instantly Rdsthomas Mission Control

Instantly.ai cold email outreach API - manage campaigns, leads, accounts, and analytics. Use for cold email automation, lead management, campaign creation/monitoring, and email account warmup.

761 votes
View all in tools →