
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
opencode-froggy
Advanced tools
OpenCode plugin with a hook layer (tool.before.*, session.idle...), agents (code-reviewer, doc-writer), and commands (/review-pr, /commit)
OpenCode plugin providing hooks, specialized agents (architect, doc-writer, rubber-duck, partner, code-reviewer, code-simplifier), skills (ask-questions-if-underspecified, tdd), and tools (gitingest, pdf-to-markdown, blockchain queries, agent-promote).
Add the plugin to your OpenCode configuration file (opencode.json):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-froggy"]
}
Alternatively, clone or copy the plugin files to one of these directories:
.opencode/plugin/opencode-froggy/~/.config/opencode/plugin/opencode-froggy/| Command | Description | Agent |
|---|---|---|
/agent-promote <name> [grade] | Promote an agent to primary (default) or specify grade: subagent, primary, all | - |
/agent-demote <name> | Demote an agent to subagent | - |
/commit-push | Stage, commit, and push changes with user confirmation | build |
/diff-summary [source] [target] | Show working tree changes or diff between branches | - |
/doc-changes | Update documentation based on uncommitted changes (new features only) | doc-writer |
/review-changes | Review uncommitted changes (staged, unstaged, untracked) | code-reviewer |
/review-pr <source> <target> | Review diff from source branch into target branch | code-reviewer |
/send-to [agent] <message> | Send a message to a child session (subagent) to continue the conversation | - |
/simplify-changes | Simplify uncommitted changes (staged, unstaged, untracked) | code-simplifier |
/tests-coverage | Run the full test suite with coverage report and suggest fixes for failures | build |
The /diff-summary command supports two modes:
Working tree mode (no parameters):
/diff-summary
Shows staged changes, unstaged changes, and untracked file contents.
Branch comparison mode (with parameters):
# Compare a branch with the current branch (HEAD)
/diff-summary feature-branch
# Compare two specific branches
/diff-summary feature-branch main
Shows stats overview, commits, files changed, and full diff between branches.
| Agent | Mode | Description |
|---|---|---|
architect | subagent | Strategic technical advisor providing high-leverage guidance on architecture, code structure, and complex engineering trade-offs. Read-only. |
doc-writer | subagent | Technical writer that crafts clear, comprehensive documentation (README, API docs, architecture docs, user guides). |
code-reviewer | subagent | Read-only code review agent for quality, correctness, security, and maintainability feedback. |
code-simplifier | subagent | Simplifies recently modified code for clarity and maintainability while strictly preserving behavior. |
partner | subagent | Strategic ideation partner that breaks frames, expands solution spaces, and surfaces non-obvious strategic options. Read-only. |
rubber-duck | subagent | Strategic thinking partner for exploratory dialogue. Challenges assumptions, asks pointed questions, and sharpens thinking through conversational friction. Read-only. |
Skills are contextual instructions loaded on demand via the skill tool. The agent invokes skill({ name: "skill-name" }) to load the instructions when needed.
| Skill | Description |
|---|---|
ask-questions-if-underspecified | Clarify requirements before implementing. Use when serious doubts arise. |
tdd | Apply Test-Driven Development workflow for new features and bugfixes. |
User request:
"Add authentication to the API"
Agent response (after loading the skill):
The agent recognizes this request has multiple interpretations and uses the question tool:
┌─ Authentication Approach ────────────────────────────────────────┐
│ │
│ Which authentication method should be implemented? │
│ │
│ ○ JWT tokens (Recommended) │
│ ○ Session-based with cookies │
│ ○ OAuth 2.0 / OpenID Connect │
│ ○ API keys │
│ ○ Other │
│ │
└──────────────────────────────────────────────────────────────────┘
After the user selects "JWT tokens", the agent confirms:
"I'll add JWT-based authentication to the API endpoints using the existing Express middleware pattern. Success = all
/api/*routes require a valid token, with a/auth/loginendpoint for token generation."
Then implementation begins.
User request:
"Add a function to calculate the total price with tax"
Agent response (after loading the skill):
The agent follows the TDD workflow:
"I'll create a function that calculates total price with tax. It should accept a price and tax rate, returning the total."
describe("calculateTotalWithTax", () => {
it("should add tax to the base price", () => {
expect(calculateTotalWithTax(100, 0.2)).toBe(120)
})
it("should handle zero tax rate", () => {
expect(calculateTotalWithTax(100, 0)).toBe(100)
})
})
function calculateTotalWithTax(price: number, taxRate: number): number {
return price * (1 + taxRate)
}
The tests are written first (red), then the minimal implementation to pass them (green).
Skills are discovered from the following locations, in order of increasing priority:
| Priority | Scope | Location |
|---|---|---|
| 1 (lowest) | plugin | <plugin>/skill/ |
| 2 | global | ~/.config/opencode/skill/ |
| 3 (highest) | project | <project>/.opencode/skill/ |
If multiple skills share the same name, the one from the highest-priority location takes precedence.
Each skill lives in its own directory with a SKILL.md file:
skill/
└── my-skill/
└── SKILL.md
The SKILL.md file uses YAML frontmatter for metadata:
---
name: my-skill
description: Short description of the skill (required)
use_when: >
Condition for automatic activation (optional).
---
# Detailed Instructions
Markdown content with step-by-step guidance...
| Field | Required | Description |
|---|---|---|
name | Yes | Unique identifier for the skill |
description | Yes | Short description (displayed in skill listings) |
use_when | No | Condition for automatic activation |
A skill without name or description will be ignored.
If a skill defines use_when, a directive is injected into the system prompt:
MANDATORY: Call skill({ name: "my-skill" }) <use_when content>
This instructs the agent to load the skill when the specified condition is met.
What is injected:
nameuse_when text (normalized: multiple spaces collapsed to single space)What is NOT injected:
descriptionFetch a GitHub repository's full content via gitingest.com. Returns summary, directory tree, and file contents optimized for LLM analysis. Use when you need to understand an external repository's structure or code.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | Yes | - | The GitHub repository URL to fetch |
maxFileSize | number | No | 50000 | Maximum file size in bytes to include |
pattern | string | No | "" | Glob pattern to filter files (e.g., *.ts, src/**/*.py) |
patternType | "include" | "exclude" | No | "exclude" | Whether to include or exclude files matching the pattern |
// Fetch entire repository
gitingest({ url: "https://github.com/user/repo" })
// Only TypeScript files
gitingest({
url: "https://github.com/user/repo",
pattern: "*.ts",
patternType: "include"
})
// Exclude test files
gitingest({
url: "https://github.com/user/repo",
pattern: "*.test.ts",
patternType: "exclude"
})
// Increase max file size to 100KB
gitingest({
url: "https://github.com/user/repo",
maxFileSize: 100000
})
maxFileSize parameter controls individual file size, not total output sizeSend a message to a child session (subagent) to continue the conversation. Useful for iterating with subagents without creating new sessions.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
message | string | Yes | - | The message to send to the child session |
sessionId | string | No | - | The child session ID to target. If omitted, targets the last child session. |
// Send a message to the last child session
promptSession({ message: "Please also add unit tests for the new function" })
// Send a message to a specific child session
promptSession({
message: "Can you clarify the error handling approach?",
sessionId: "abc123"
})
sessionId is not provided, the tool automatically targets the most recently created child sessionList all child sessions (subagents) of the current session. Useful for finding specific sessions to target with prompt-session.
This tool takes no parameters.
// List all child sessions
listChildSessions()
Returns a formatted list of child sessions with:
Example output:
Child sessions (2):
1. [abc123] Code Review Session
Created: 2024-01-15T10:30:00Z | Updated: 2024-01-15T10:35:00Z
2. [def456] Architecture Discussion
Created: 2024-01-15T11:00:00Z | Updated: 2024-01-15T11:15:00Z
Convert a text-based PDF into enriched Markdown (headings, paragraphs, lists). Returns Markdown as plain text.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
filePath | string | Yes | - | Absolute path to the PDF file to convert |
maxPages | number | No | All pages | Maximum number of pages to convert (positive integer) |
// Convert an entire PDF
pdfToMarkdown({ filePath: "/path/to/file.pdf" })
// Convert the first 3 pages
pdfToMarkdown({
filePath: "/path/to/file.pdf",
maxPages: 3
})
maxPages is capped at the document's total page count.Promote an agent to primary (default) or specify a grade.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Name of the plugin agent (e.g., rubber-duck, architect) |
grade | string | No | Target type: subagent, primary, or all (default: primary) |
| Grade | Effect |
|---|---|
subagent | Available only as a subagent |
primary | Appears in Tab selection for direct use |
all | Available both as primary and subagent |
# Promote rubber-duck to primary (default)
/agent-promote rubber-duck
# Promote with explicit grade
/agent-promote architect all
# Demote back to subagent
/agent-demote rubber-duck
Tab or <leader>a to select the agentTools for querying Ethereum and EVM-compatible blockchains via Etherscan APIs.
All blockchain tools support multiple chains via the chainId parameter:
| Chain ID | Network |
|---|---|
1 | Ethereum (default) |
137 | Polygon |
56 | BSC |
42161 | Arbitrum |
10 | Optimism |
8453 | Base |
43114 | Avalanche |
250 | Fantom |
324 | zkSync |
The blockchain tools use Etherscan-compatible APIs. An API key is optional but recommended.
Environment Variable:
| Variable | Required | Description |
|---|---|---|
ETHERSCAN_API_KEY | No | API key for Etherscan and compatible explorers |
Without an API key: Requests are rate-limited (typically 1 request per 5 seconds).
With an API key: Higher rate limits and more reliable access.
Getting an API key:
Setting the environment variable:
export ETHERSCAN_API_KEY="your-api-key-here"
Get Ethereum transaction details by transaction hash. Returns status, block, addresses, gas costs in JSON format. Use optional parameters to include internal transactions, token transfers, and decoded event logs.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
hash | string | Yes | - | Transaction hash (0x...) |
chainId | string | No | "1" | Chain ID (see table above) |
includeInternalTxs | boolean | No | false | Include internal transactions (ETH transfers between contracts) |
includeTokenTransfers | boolean | No | false | Include ERC-20 token transfers |
decodeLogs | boolean | No | false | Decode event logs (Transfer, Approval, Deposit, Withdrawal) |
// Get basic transaction details on Ethereum mainnet
ethTransaction({ hash: "0x123abc..." })
// Get transaction on Polygon
ethTransaction({
hash: "0x123abc...",
chainId: "137"
})
// Get transaction with internal transactions and token transfers
ethTransaction({
hash: "0x123abc...",
includeInternalTxs: true,
includeTokenTransfers: true
})
// Get full transaction details with decoded event logs
ethTransaction({
hash: "0x123abc...",
includeInternalTxs: true,
includeTokenTransfers: true,
decodeLogs: true
})
The tool returns JSON with labeled addresses (contract names resolved via Etherscan):
{
"hash": "0x123...",
"status": "success",
"block": 12345678,
"from": { "address": "0xabc...", "label": "Uniswap V3: Router" },
"to": { "address": "0xdef...", "label": "WETH" },
"value": "0",
"gas": { "used": 150000, "price": "20000000000", "cost": "0.003" }
}
With includeInternalTxs: true:
{
"internalTransactions": [
{
"from": { "address": "0x...", "label": "Uniswap V3: Router" },
"to": { "address": "0x...", "label": null },
"value": "1.5",
"type": "call"
}
]
}
With includeTokenTransfers: true:
{
"tokenTransfers": [
{
"token": { "address": "0x...", "name": "Wrapped Ether", "symbol": "WETH", "decimals": 18 },
"from": { "address": "0x...", "label": null },
"to": { "address": "0x...", "label": "Uniswap V3: Router" },
"value": "1.5"
}
]
}
With decodeLogs: true:
{
"decodedEvents": [
{
"name": "Transfer",
"address": { "address": "0x...", "label": "WETH" },
"params": { "from": "0x...", "to": "0x...", "value": "1500000000000000000" }
}
],
"undecodedEventsCount": 2
}
| Event | Description |
|---|---|
Transfer | ERC-20 token transfer |
Approval | ERC-20 approval for spending |
Deposit | WETH deposit (ETH → WETH) |
Withdrawal | WETH withdrawal (WETH → ETH) |
Get the ETH balance of an Ethereum address. Returns balance in both ETH and Wei.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
address | string | Yes | - | Ethereum address (0x...) |
chainId | string | No | "1" | Chain ID (see table above) |
// Get balance on Ethereum mainnet
ethAddressBalance({ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" })
// Get balance on Arbitrum
ethAddressBalance({
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
chainId: "42161"
})
List Ethereum transactions for an address. Shows incoming and outgoing transactions with values, timestamps, and status.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
address | string | Yes | - | Ethereum address (0x...) |
limit | number | No | 20 | Maximum number of transactions to return |
chainId | string | No | "1" | Chain ID (see table above) |
// List recent transactions on Ethereum mainnet
ethAddressTxs({ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" })
// List last 50 transactions on Base
ethAddressTxs({
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
limit: 50,
chainId: "8453"
})
List ERC-20 token transfers for an Ethereum address. Shows token names, symbols, values, and transaction details.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
address | string | Yes | - | Ethereum address (0x...) |
limit | number | No | 20 | Maximum number of transfers to return |
chainId | string | No | "1" | Chain ID (see table above) |
// List recent token transfers on Ethereum mainnet
ethTokenTransfers({ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" })
// List last 100 token transfers on Optimism
ethTokenTransfers({
address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
limit: 100,
chainId: "10"
})
Hooks run actions on session events. Configuration is loaded from standard OpenCode configuration directories.
Hooks are loaded from these locations (in order, merged together):
| Platform | Global | Project |
|---|---|---|
| Linux | ~/.config/opencode/hook/hooks.md | <project>/.opencode/hook/hooks.md |
| macOS | ~/.config/opencode/hook/hooks.md | <project>/.opencode/hook/hooks.md |
| Windows | ~/.config/opencode/hook/hooks.md or %APPDATA%/opencode/hook/hooks.md | <project>/.opencode/hook/hooks.md |
On Windows, ~/.config is preferred for cross-platform consistency. If hooks exist in %APPDATA% but not in ~/.config, the %APPDATA% location is used.
Global hooks run first, then project hooks are added. Hooks from both sources are combined (not overridden).
hooks listevent, actions, and optional conditionsExample hooks.md:
---
hooks:
- event: session.idle
conditions: [hasCodeChange, isMainSession]
actions:
- command: simplify-changes
---
| Event | Description |
|---|---|
session.idle | Emitted when a session becomes idle and has files modified via write or edit in that session |
session.created | Emitted when a session is created |
session.deleted | Emitted when a session is deleted |
tool.before.* | Emitted before any tool executes. Exit code 2 blocks the tool. |
tool.before.<name> | Emitted before a specific tool (e.g., tool.before.write). Exit code 2 blocks the tool. |
tool.after.* | Emitted after any tool executes |
tool.after.<name> | Emitted after a specific tool (e.g., tool.after.edit) |
tool.before.* (all tools)tool.before.<name> (specific tool)tool.after.* (all tools)tool.after.<name> (specific tool)For tool.before.* and tool.before.<name> hooks, a bash action returning exit code 2 will block the tool from executing. The stderr output is displayed to the user as the block reason.
| Condition | Description |
|---|---|
isMainSession | Run only for the main session (not sub-sessions) |
hasCodeChange | Run only if at least one modified file looks like code |
All listed conditions must pass for the hook to run.
Code extensions treated as "code":
ts, tsx, js, jsx, mjs, cjs, json, yml, yaml, toml, css, scss, sass, less, html, vue, svelte, go, rs, c, h, cpp, cc, cxx, hpp, java, py, rb, php, sh, bash, kt, kts, swift, m, mm, cs, fs, scala, clj, hs, lua.
Execute a plugin command.
# Short form
- command: simplify-changes
# With arguments
- command:
name: review-pr
args: "main feature"
If the command exists in config, the plugin reuses its agent and model.
Prompt the session to use a tool with specific arguments.
- tool:
name: bash
args: { command: "echo done" }
Execute a shell command directly without involving the LLM. Useful for running linters, formatters, build scripts, or custom automation.
Configuration:
# Short form
- bash: "npm run lint"
# Long form with custom timeout
- bash:
command: "$OPENCODE_PROJECT_DIR/.opencode/hooks/init.sh"
timeout: 30000 # milliseconds (default: 60000)
Environment Variables:
The plugin injects these variables into the child process environment:
| Variable | Value | Use case |
|---|---|---|
OPENCODE_PROJECT_DIR | Absolute path to the project (e.g., /home/user/project) | Reference project files from scripts located elsewhere |
OPENCODE_SESSION_ID | The OpenCode session identifier | Logging, tracing, or conditioning actions based on session |
Stdin JSON Context:
The command receives a JSON object via stdin with session context:
{
"session_id": "abc123",
"event": "session.idle",
"cwd": "/path/to/project",
"files": ["src/index.ts", "src/utils.ts"]
}
The files array is only present for session.idle events and contains paths modified via write or edit.
For tool hooks (tool.before.*, tool.after.*), additional fields are provided:
{
"session_id": "abc123",
"event": "tool.before.write",
"cwd": "/path/to/project",
"tool_name": "write",
"tool_args": { "filePath": "src/index.ts", "content": "..." }
}
Environment Variables vs Stdin JSON:
$VAR, convenient for simple values like paths and IDsjq or similarBoth mechanisms are complementary. Use environment variables for quick access to project path and session ID; use stdin JSON when you need event details or the list of modified files.
Exit Codes:
| Code | Behavior |
|---|---|
0 | Success, continue to next action |
2 | Blocking error, stop remaining actions in this hook |
| Other | Non-blocking error, log warning and continue |
Result Feedback:
Bash hook results are automatically sent back to your session:
[BASH HOOK ✓] npm run lint
Exit: 0 | Duration: 1234ms
Stdout: All files passed linting
The feedback includes a status icon (✓ success, ✗ failure), exit code, execution duration, and stdout/stderr output (truncated to 500 characters). This message appears in your session but does not trigger a response from the assistant.
session.idle only fires if files were modified via write or edit; the session's modified file list is cleared after the hook runssession.created with no parent, or on the first session.idle if needed---
hooks:
- event: session.idle
conditions: [hasCodeChange, isMainSession]
actions:
- bash: "npm run lint --fix"
- command: simplify-changes
- event: session.created
actions:
- bash:
command: "$OPENCODE_PROJECT_DIR/.opencode/hooks/init.sh"
timeout: 30000
- command:
name: review-pr
args: "main feature"
---
hooks:
- event: session.idle
conditions: [hasCodeChange]
actions:
- bash: "npm run lint --fix"
.opencode/hooks/init.sh:
#!/bin/bash
set -e
# Read JSON context from stdin
context=$(cat)
session_id=$(echo "$context" | jq -r '.session_id')
event=$(echo "$context" | jq -r '.event')
cwd=$(echo "$context" | jq -r '.cwd')
echo "Session $session_id triggered $event in $cwd"
# Use environment variables
echo "Project: $OPENCODE_PROJECT_DIR"
# Exit 0 for success, 2 to block remaining actions
exit 0
#!/bin/bash
# Run linter and block if critical errors found
if ! npm run lint 2>&1 | grep -q "critical"; then
exit 0 # Success, continue
else
echo "Critical lint errors found, blocking further actions"
exit 2 # Block remaining actions
fi
hooks:
- event: tool.before.write
actions:
- bash: |
file=$(cat | jq -r '.tool_args.filePath // .tool_args.file_path // .tool_args.path')
if echo "$file" | grep -qE '\.(env|pem|key)$'; then
echo "Cannot modify sensitive files: $file" >&2
exit 2
fi
- event: tool.before.edit
actions:
- bash: |
file=$(cat | jq -r '.tool_args.filePath // .tool_args.file_path // .tool_args.path')
if echo "$file" | grep -qE '\.(env|pem|key)$'; then
echo "Cannot modify sensitive files: $file" >&2
exit 2
fi
hooks:
- event: tool.after.write
actions:
- bash: |
file=$(cat | jq -r '.tool_args.filePath // .tool_args.file_path // .tool_args.path')
if echo "$file" | grep -qE '\.tsx?$'; then
npx prettier --write "$file"
fi
hooks:
- event: tool.before.*
actions:
- bash: |
context=$(cat)
tool=$(echo "$context" | jq -r '.tool_name')
echo "[$(date)] Tool: $tool" >> /tmp/opencode-tools.log
The plugin does not require additional configuration. Agents, commands, and skills are loaded automatically from the agent/, command/, and skill/ directories within the plugin. Hooks are loaded from the standard OpenCode configuration directories (see Hooks section).
The hasCodeChange condition checks file extensions against the default set listed in the Conditions section. Hooks without any conditions still trigger on any modified file paths tracked via write or edit in the current session.
MIT
FAQs
OpenCode plugin with a hook layer (tool.before.*, session.idle...), agents (code-reviewer, doc-writer), and commands (/review-pr, /commit)
We found that opencode-froggy demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.