Install
$ agentstack add skill-keugenek-shark-shark-exec ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ● Dynamic code execution Used
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
shark-exec
Never block the main turn. This skill wraps any slow shell command in a background process + poller, so the agent always replies to the user within 30 seconds — even if the command takes 10 minutes.
When to Use
Use this skill whenever you're about to call a shell command and the command is expected to take more than ~5 seconds. Examples:
gh run watch— waiting for CInpm run build/pytest/cargo builddocker build,docker pull- Long-running remote commands
- Any command with a long blocking wait
- Any command that polls, watches, or tails output
Do NOT use for:
- Quick reads (
cat,ls,git status) — inline is fine - Commands you're confident finish in /skills/shark/shark-exec/state/pending.json
. If it doesn't exist, start with{"jobs": []}`.
Append your new job:
{
"jobs": [
{
"sessionId": "sess-abc-123",
"label": "CI: run #12345",
"command": "gh run watch 12345",
"startedAt": 1710000000000,
"maxSeconds": 120,
"cronJobId": null
}
]
}
Critical: startedAt must be the actual current timestamp in milliseconds (Date.now()), not a hardcoded placeholder.
Set cronJobId: null for now — you'll fill it in step 4.
Step 4 — Schedule a Poller
Schedule a poller that fires every ~15 seconds. The poller reads pending.json, checks each job's status, delivers results, and cleans up completed jobs.
Generic pseudocode:
scheduler.every(15000, () => {
jobs = read("pending.json").jobs
for job in jobs:
status = poll(job.handle)
if status.done:
notify_user(job.label, status.output)
remove_job(job)
elif now > job.startedAt + job.maxSeconds * 1000:
kill(job.handle)
notify_user(job.label, "killed after Xs", status.partial_output)
remove_job(job)
if jobs.empty:
cancel_this_scheduler()
})
OpenClaw-specific example:
{
"schedule": {"kind": "every", "everyMs": 15000},
"payload": {
"kind": "agentTurn",
"message": "Check /skills/shark/shark-exec/state/pending.json for pending background jobs. For each entry: call process(action=poll, sessionId=X, timeout=3000). If completed, send the result to and remove the entry from pending.json. If still running and startedAt + maxSeconds*1000 **Important:** Only create ONE poller per session, even if there are multiple concurrent background jobs. The single poller will check all entries in pending.json.
### Step 5 — Poller Fires (Every 15s)
The poller will:
1. Read `pending.json`
2. For each job, check its process status
3. If **completed**: send result to user, close the finished handle if your runtime requires explicit cleanup, remove from jobs array, save pending.json
4. If **still running** and **past maxSeconds**: kill the process, send partial output + timeout message
5. If **still running** and within maxSeconds: leave in place, poller will retry in 15s
6. If jobs array is empty after processing: cancel the poller
### Step 6 — Result Delivery Format
**Success:**
✅ CI: run #12345 completed (47s)
**Timeout/Kill:**
⏱ CI: run #12345 killed after 120s
Last output:
**Process already exited before first poll** (common when the command finishes in
In this case, the exec result may arrive as a system event in the main session. Read it from there and deliver it directly — no need for the poller at all.
If your runtime keeps completed agents around until you explicitly tear them down, close them at this point too. In Codex, a completed subagent should be close_agent(id)'d after its result has been delivered unless you are intentionally keeping it for reuse.
Error (process not found / session lost):
❌ CI: run #12345 — session not found (process may have exited before poll; check last system event for output)
State File Format
Path: /skills/shark/shark-exec/state/pending.json
{
"jobs": [
{
"sessionId": "sess-abc-123",
"label": "CI: run #12345",
"command": "gh run watch 12345",
"startedAt": 1710000000000,
"maxSeconds": 120,
"cronJobId": "cron-xyz-456"
}
]
}
| Field | Type | Description | |-------|------|-------------| | sessionId | string | From exec response (or PID for non-OpenClaw runtimes) | | label | string | Human-readable name shown in ack/result | | command | string | The shell command that was run | | startedAt | number | Date.now() at launch time (ms) | | maxSeconds | number | Kill threshold (default: 120) | | cronJobId | string\|null | Poller/cron job ID for cleanup; null until created |
The state file format is agent-agnostic — any runtime adapter can read and write it.
maxSeconds Defaults
| Command type | Suggested maxSeconds | |---|---| | gh run watch | 300 (CI can be slow) | | npm run build | 180 | | docker build | 600 | | pytest / cargo test | 300 | | Generic unknown | 120 | | User-specified | Honor their request |
If the user says "wait up to 10 minutes", use maxSeconds: 600.
Multiple Concurrent Jobs
If there's already a poller running (check cronJobId in any existing job in pending.json), do not create a new poller. Just add your new job to the array. The existing poller will pick it up on its next tick.
Algorithm:
- Read pending.json
- If
jobs.length > 0and any job has a non-nullcronJobId→ reuse that cronJobId, just append new job - If
jobs.length === 0or allcronJobIdare null → create a new poller, then update state
Runtime Adapters
OpenClaw
- Background exec:
exec({background: true, yieldMs: 500}) - Poll:
process({action: "poll", sessionId: X, timeout: 5000}) - Schedule:
cron({action: "add", schedule: {kind: "every", everyMs: 15000}, ...}) - Notify:
message({action: "send", target: "", message: "..."})
Claude Code / claude --print
- Background exec:
Bash("command &")+ capture PID - Poll:
Bash("kill -0 && cat /tmp/output-.txt") - Schedule: not native — use a wrapper script or OS cron
- Notify: write result to stdout (caller receives it)
Codex (openai/gpt-5-codex)
- Background exec:
shell("command &")+ PID - Poll:
shell("ps -p ; cat /tmp/out-.txt") - Schedule: OS cron or a watcher script
- Agent remoras:
spawn_agent(...)→wait_agent(...)→ deliver result →close_agent(id)unless you are intentionally reusing that same agent
Cursor / Windsurf / Aider
- Background exec: terminal background process (
&orStart-Jobon Windows) - Poll: check process status + output file
- Schedule: OS-level cron or task scheduler
Error Handling
Poll throws "session not found"
→ Remove the job from pending.json, send: ❌ [label] — session lost (process may have crashed or the exec session expired)
Completed agent still hanging around
→ If the work is done and the runtime still shows the subagent as open, close it as part of delivery cleanup. In Codex, use close_agent(id) after a completed wait_agent(...) unless you plan to reuse that agent.
Output is very long
→ Truncate to last 50 lines. Always append truncation notice: [output truncated — showing last 50 lines of N total]
pending.json is corrupted/invalid JSON
→ Reset to {"jobs": []}, send: ⚠️ shark-exec: pending.json was corrupted and has been reset. Background jobs may have been lost.
exec returns no handle/sessionId
→ Fall back to inline exec. Do not use shark-exec for that command.
Full Example: Replacing gh run watch
❌ Old (blocking) way:
exec("gh run watch 12345")
// Agent blocks for 3 minutes, user gets no reply
✅ New (shark-exec) way:
Turn 1 (main agent):
- Send:
⏳ CI: run #12345 — watching in background (max 300s)... - Launch background:
exec("gh run watch 12345", background=true, yieldMs=500)→sessionId: "sess-9f3a" - Write to pending.json:
``json { "jobs": [{ "sessionId": "sess-9f3a", "label": "CI: run #12345", "command": "gh run watch 12345", "startedAt": 1710005200000, "maxSeconds": 300, "cronJobId": null }] } ``
- Create poller (every 15s, using your agent's scheduler) →
cronJobId: "cron-8b2c" - Update pending.json with
cronJobId: "cron-8b2c" - **Main turn ends. User got their reply in 5s? → Use shark-exec
- [ ] Send ack message first
- [ ] Launch in background (see Runtime Adapters)
- [ ] Write to pending.json
- [ ] Create or reuse poller
- [ ] Update cronJobId in state
- [ ] Close completed agents/handles if the runtime requires explicit cleanup
Helper Script
scripts/poll-and-deliver.js — run to inspect current pending jobs:
node /skills/shark/shark-exec/scripts/poll-and-deliver.js
Prints a human-readable summary of all pending jobs, their ages, and whether they're past maxSeconds. Useful for debugging stuck jobs.
> Note: This script is the OpenClaw adapter. For other runtimes, implement equivalent polling using your platform's APIs (see Runtime Adapters above).
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: keugenek
- Source: keugenek/shark
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.