{"id":50817225,"url":"https://github.com/betarixm/catchy","last_synced_at":"2026-06-13T10:33:16.536Z","repository":{"id":356479695,"uuid":"1232430737","full_name":"betarixm/catchy","owner":"betarixm","description":"Ca-ca-catch my flag, baby.","archived":false,"fork":false,"pushed_at":"2026-05-15T19:49:48.000Z","size":1665,"stargazers_count":4,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-15T21:52:23.418Z","etag":null,"topics":["agent","codex","ctf","cybersecurity","harness","llm","openai"],"latest_commit_sha":null,"homepage":"https://catchy.bxta.kr","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/betarixm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-07T23:35:52.000Z","updated_at":"2026-05-15T19:49:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/betarixm/catchy","commit_stats":null,"previous_names":["betarixm/catchy"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/betarixm/catchy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/betarixm%2Fcatchy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/betarixm%2Fcatchy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/betarixm%2Fcatchy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/betarixm%2Fcatchy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/betarixm","download_url":"https://codeload.github.com/betarixm/catchy/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/betarixm%2Fcatchy/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34281700,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","codex","ctf","cybersecurity","harness","llm","openai"],"created_at":"2026-06-13T10:33:15.024Z","updated_at":"2026-06-13T10:33:16.511Z","avatar_url":"https://github.com/betarixm.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# 🪤\u003cbr\u003eCatchy\n\n**Ca-ca-catch my flag, baby.**\n\nAutonomous AI agent runner for capture-the-flag challenges.\n\n\u003csub\u003e[Web](https://catchy.bxta.kr)\u003c/sub\u003e\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n\u003cimg src=\"assets/web.png\" alt=\"Catchy web\" width=\"900\" /\u003e\n\n\u003c/div\u003e\n\n## What is this\n\nCatchy plugs an agent into a CTF challenge, runs it inside a sandboxed workspace, and streams reasoning steps, commands, file changes, costs, and run state into a Django web UI. Each challenge can have multiple threads, and each thread gets its own source extraction, writable workspace, metadata directory, agent configuration, model, credential, and event log.\n\n## Quick start\n\n```bash\n# 1. Install dependencies — uv handles the workspace + venv\nuv sync\n\n# 2. Set your OpenAI API key, or add it later as a web credential\nexport OPENAI_API_KEY=sk-...\n\n# 3. Prepare the Django database\nuv run python -m catchy.web.manage migrate\nuv run python -m catchy.web.manage createsuperuser\n\n# 4. Start the web app\nuv run python -m catchy.web.manage runserver\n```\n\nOpen \u003chttp://127.0.0.1:8000\u003e, sign in, then create a credential, model, agent configuration, CTF, and challenge. Start a thread from the challenge page to run the agent and stream its output.\n\n\u003e **Requires** Python 3.14+, [`uv`](https://docs.astral.sh/uv/), and a running Docker daemon.\n\n## Web setup\n\nThe web UI stores reusable configuration in the database:\n\n- **Credentials** hold provider API keys. Agent YAML can reference them with OmegaConf interpolation such as `${credential:openai}`.\n- **Models** name the model that should be injected into a run.\n- **Agents** store YAML like `configurations/codex.yaml` or `configurations/claude-code.yaml`; the `class` field should be a fully qualified import path such as `catchy.codex.CodexAgent` or `catchy.claude_code.ClaudeCodeAgent`.\n- **CTFs** group challenges and access rules.\n- **Challenges** include a markdown description, optional webhook settings, optional runner config, and a source archive upload or download URL.\n\n## Anatomy of a challenge\n\nChallenges are stored in the web database with an uploaded or downloaded source archive. When a thread starts, Catchy extracts that archive and creates separate source, workspace, and metadata directories for the run.\n\n```text\nmedia/\n└── threads/\n    └── thread-.../\n        ├── source/     # extracted challenge archive\n        ├── workspace/  # writable scratchpad mounted into the agent container\n        └── metadata/   # run metadata and artifacts kept separate from workspace\n```\n\nWhile a thread is active, use the thread page to queue prompts, steering messages, or a stop request. Public threads can be shared from the web UI.\n\n## Agent Configuration\n\nAgent configurations live in `configurations/*.yaml`. The `class` field is a fully qualified Python import path; Catchy imports it dynamically, validates the YAML with that module's `Configuration` model, then calls `AgentClass.from_configuration(...)`.\n\n```yaml\n# configurations/codex.yaml\nid: codex-gpt-5.5\nclass: catchy.codex.CodexAgent\nmodel:\n  provider: openai\n  name: gpt-5.5\n  api_key: ${oc.env:OPENAI_API_KEY}\n```\n\nThe old shorthand `class: CodexAgent` still resolves to `catchy.codex.CodexAgent`, but new configs should use the full import path.\n\n## Project layout\n\n```text\ncatchy/\n├── packages/\n│   ├── core/         # Challenge, Agent, Webhook protocols \u0026 models\n│   ├── claude-code/  # ClaudeCodeAgent — Claude Code + Docker runtime\n│   ├── codex/        # CodexAgent — Codex App Server + Docker runtime\n│   └── web/          # Django web UI and thread orchestration\n├── configurations/   # Agent YAML configurations\n├── challenges/       # Example challenge definitions and source files\n└── assets/           # Screenshots and images\n```\n\n## Adding a new agent\n\nThe `Agent` protocol is minimal: implement `stream(...)`, add a Pydantic-style `Configuration` model in the same module, and expose `from_configuration(...)` on the agent class. `stream(...)` is an async generator: it yields display text and can receive `str | None` steering messages between yields.\n\n```python\nfrom pathlib import Path\nfrom typing import AsyncGenerator\n\nfrom pydantic import BaseModel\n\nfrom catchy.core.agent.protocols import Agent\nfrom catchy.core.challenge.models import Challenge\nfrom catchy.core.webhook.models import Webhook\n\nclass Configuration(BaseModel):\n    id: str\n\nclass MyAgent(Agent):\n    key = \"my-agent\"\n\n    def __init__(self, id: str):\n        self.id = id\n\n    @staticmethod\n    def from_configuration(configuration: Configuration) -\u003e \"MyAgent\":\n        return MyAgent(id=configuration.id)\n\n    async def stream(\n        self,\n        challenge: Challenge,\n        workspace: Path,\n        metadata_directory: Path,\n        webhook: Webhook | None = None,\n    ) -\u003e AsyncGenerator[str, str | None]:\n        steering_message = yield \"thinking...\"\n        if steering_message is not None:\n            ...\n        ...\n```\n\nDrop it under `packages/\u003cname\u003e/`, register it in the workspace, then add a YAML file:\n\n```yaml\nid: my-agent\nclass: catchy.my_agent.MyAgent\n```\n\n## Roadmap\n\n- [ ] Additional agents (Claude Code, custom)\n- [ ] Exportable run transcripts\n- [ ] Per-challenge scoreboard\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbetarixm%2Fcatchy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbetarixm%2Fcatchy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbetarixm%2Fcatchy/lists"}