Skip to content

Skill should use CLI subcommands instead of inline python -c #197

Description

@bsouthwood

Problem

The Claude Code skill (skill.md) passes all pipeline steps as inline Python via python -c "...". This triggers multiple security heuristics in Claude Code:

  1. Command substitution — $(cat graphify-out/.graphify_python) to resolve the interpreter triggers "Contains command_substitution" prompts
  2. Comments in arguments — multi-line Python with # comments triggers "Newline followed by # inside a quoted argument can hide arguments from path validation"

These prompts cannot be auto-accepted via Claude Code's permission allow-rules, so users must click "Yes" on every single step (~30 times per pipeline run). This makes the skill nearly unusable without manually patching the skill file.

Root cause

The skill uses Bash as a code delivery mechanism. Every step is:

$(cat graphify-out/.graphify_python) -c "
import json
from graphify.detect import detect
# ... 10-20 lines of Python
"

This is the wrong abstraction layer. The Python code should live behind CLI entry points, not inside shell strings.

What already works

graphify query was added as a CLI subcommand in v0.3.8 (#43), and graphify hook, graphify save-result, graphify claude also exist as CLI commands. But the skill doesn't use graphify query — it still constructs inline Python for queries.

Proposed fix

Expose the remaining pipeline steps as CLI subcommands:

graphify detect <path>                    # Step 2
graphify extract <path>                   # Step 3 (AST + semantic dispatch)
graphify build                            # Step 4
graphify cluster                          # Step 4 (or --cluster-only)
graphify label                            # Step 5
graphify export --html --obsidian --svg   # Step 6-7
graphify benchmark                        # Step 8

Then update skill.md to call these instead of inline Python. Benefits:

  • No security prompts — graphify detect . is a normal command, no heuristics triggered
  • Scoped permissions — users can allow Bash(graphify *) which is constrained to what the CLI exposes, not arbitrary Python execution
  • No injection risk — arguments are CLI args parsed by argparse, not interpolated into Python strings
  • Simpler skill — the skill becomes a sequence of CLI calls instead of a Python code generator

Workarounds users are currently doing

Environment

  • graphifyy 0.4.1
  • Claude Code (CLI)
  • Ubuntu 24.04

Activity

  1. biggates commented on Apr 16, 2026

    @biggates

    Previous issue #114 does not solve all of them.

  2. nothariharan commented on Oct 10, 2026

    @nothariharan
    Contributor

    Fixed in #4275 — and here's exactly what was wrong, how it's fixed, and what's deliberately left out.

    Why #114 didn't cover this

    #114 replaced one block — the inline save_query_result Python — with graphify save-result (v0.3.18), then closed. Every other pipeline step and every on-demand reference flow kept its inline Python. biggates' follow-up comment in #114 flagged exactly that ("there are still a lot of other raw python scripts in skills… are you going to do all of them?"), but it was never turned into work — which is what this issue tracks.

    Root cause

    Each step ran as:

    $(cat graphify-out/.graphify_python) -c "
    import json
    from graphify.detect import detect
    # ... 10-20 lines of Python with # comments
    "

    Two Claude Code heuristics fire on that, once per step, and neither is allowlistable: command_substitution (the $(cat …)) and "newline followed by # in a quoted argument" (the Python comments). ~30 manual approvals per run.

    How it's fixed

    Every step is now a plain console command. The step bodies moved into a new graphify/pipeline.py and are exposed as graphify pipeline <step> — detect, extract-ast, empty-semantic, cache-check, merge-chunks, save-cache, merge-semantic, merge-extraction, build, diagnose, label, save-manifest, cleanup, plus vocab, transcribe, detect-incremental, code-only-check, empty-extract, update-merge, graph-diff for the reference flows. The references now call those plus existing commands (graphify query/path/explain/save-result/add/watch) and the graphify-mcp console script.

    Result: no $(cat graphify-out/.graphify_python), no inline python -c, and Bash(graphify *) is now allowlistable — so the ~30 prompts per run are gone and the injection surface shrinks (arguments go through argparse instead of string interpolation).

    The skill is generated from tools/skillgen/fragments/; the edits are in the fragments and all generated artifacts were regenerated + blessed, so nothing drifts.

    Verified

    • Old vs new produce byte-identical .graphify_* sidecars, graph.json, manifest.json, and GRAPH_REPORT.md on a real corpus — same graph, different delivery.
    • Full test suite: no new failures vs the pre-change baseline.
    • New tests cover the pipeline steps end to end (including the incremental --update flow and the #479 shrink guard) and a drift guard that fails if any split host or reference reintroduces an inline -c block.
    • Reviewed twice by a separate agent; all findings fixed.

    What's intentionally not in this PR

    skill-aider.md and skill-devin.md still contain inline Python (37 and 39 blocks). That's not Claude Code — those are the Aider and Devin hosts, which #197 doesn't report. They're also a genuinely wide change: unlike the split hosts, they aren't generated from core.md + references — each is a hand-maintained ~1,300–1,400-line monolith that inlines everything, and they're frozen against a pinned v8 blob by monolith_roundtrip, which requires every changed line to match a sanctioned-diff predicate. Converting them means rewriting 76 blocks and adding a new sanctioned-diff predicate and re-validating the round-trip. That's a clean follow-up PR on its own; folding it in would have ballooned the blast radius of this one.

    So: #197 (Claude Code) is fully resolved; the aider/devin monoliths are the one tracked follow-up. Step 1's one-time interpreter detection still uses $(…) — that's inherent to installing/detecting graphify, and it's a single block, not per-step.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions