Container
An isolated environment with persistent project files.
Rootless Podman + composeRun your agents in persistent workspaces. Get notified when they need you or finish a task, then review and act from any device.


Start a task Step away Get notified Review and act
Benchyard provides the workspace; your agent does the task. Explore this coding example.
Add a status filter
Add a status filter to the issue board and test it.
I will add a status filter to the issue board, keep the counts in sync, and cover the filter behavior with tests.
update /data/workspaces/fe892d3add33/project/filter.mjsfilter.mjs
update /data/workspaces/fe892d3add33/project/filter.test.mjsfilter.test.mjs
update /data/workspaces/fe892d3add33/project/index.htmlindex.html
The filter and tests are ready. Please approve npm test to check the four filter cases before reviewing the running app.
npm test$ npm test
> test
> node --test filter.test.mjs && node -e "require('fs').writeFileSync('.test-passed','yes')"
TAP version 13
# Subtest: all includes every issue
ok 1 - all includes every issue
---
duration_ms: 2.288419
type: 'test'
...
# Subtest: in progress excludes completed issues
ok 2 - in progress excludes completed issues
---
duration_ms: 1.867077
type: 'test'
...
# Subtest: done includes only completed issues
ok 3 - done includes only completed issues
---
duration_ms: 0.637119
type: 'test'
...
# Subtest: empty board stays empty
ok 4 - empty board stays empty
---
duration_ms: 0.399472
type: 'test'
...
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 497.635526Approved from iPhone
Done. I ran npm test
Implemented the status filter.
Open port 4173 from Ports to try the updated issue board.
Approval needed
Shell npm test
Tell the agent what to do
No changes yet.
filter.mjs
| @@ -1,3 +1,5 @@ | ||
| 1 | - export function filterIssues(issues) { | |
| 2 | - return issues; | |
| 1 | + /** Return the issues matching the selected status. */ | |
| 2 | + export function filterIssues(issues, status = 'all') { | |
| 3 | + if (status === 'all') return issues; | |
| 4 | + return issues.filter(issue => issue.status === status); | |
| 3 | 5 | } |
filter.test.mjs
| @@ -1 +1,8 @@ | ||
| 1 | - // Tests for issue-board filters. | |
| 1 | + import test from 'node:test'; | |
| 2 | + import assert from 'node:assert/strict'; | |
| 3 | + import { filterIssues } from './filter.mjs'; | |
| 4 | + const issues = [{ id: 1, status: 'progress' }, { id: 2, status: 'done' }]; | |
| 5 | + test('all includes every issue', () => assert.equal(filterIssues(issues).length, 2)); | |
| 6 | + test('in progress excludes completed issues', () => assert.deepEqual(filterIssues(issues, 'progress'), [issues[0]])); | |
| 7 | + test('done includes only completed issues', () => assert.deepEqual(filterIssues(issues, 'done'), [issues[1]])); | |
| 8 | + test('empty board stays empty', () => assert.deepEqual(filterIssues([], 'done'), [])); |
index.html
| @@ -130,7 +130,7 @@ | ||
| 130 | 130 | </div> |
| 131 | 131 | <span id="count">6 issues</span> |
| 132 | 132 | </div> |
| 133 | - <!-- FILTER --> | |
| 133 | + <nav aria-label="Issue status"><button data-status="all" aria-pressed="true">All issues</button><button data-status="progress" aria-pressed="false">In progress</button><button data-status="done" aria-pressed="false">Done</button></nav> | |
| 134 | 134 | <article data-status="progress"> |
| 135 | 135 | <small>WEB-142</small> |
| 136 | 136 | <div> |
| @@ -181,5 +181,17 @@ | ||
| 181 | 181 | </article> |
| 182 | 182 | <footer>Demo project · Fictional people and issues</footer> |
| 183 | 183 | </main> |
| 184 | - <!-- SCRIPT --> | |
| 184 | + <script type="module"> | |
| 185 | + import { filterIssues } from './filter.mjs'; | |
| 186 | + const rows = [...document.querySelectorAll('article')]; | |
| 187 | + const issues = rows.map((row, id) => ({ id, status: row.dataset.status })); | |
| 188 | + for (const button of document.querySelectorAll('nav button')) { | |
| 189 | + button.onclick = () => { | |
| 190 | + const visible = new Set(filterIssues(issues, button.dataset.status).map(issue => issue.id)); | |
| 191 | + rows.forEach((row, id) => row.hidden = !visible.has(id)); | |
| 192 | + document.querySelector('#count').textContent = visible.size + ' issues'; | |
| 193 | + document.querySelectorAll('nav button').forEach(item => item.setAttribute('aria-pressed', String(item === button))); | |
| 194 | + }; | |
| 195 | + } | |
| 196 | + </script> | |
| 185 | 197 | </html> |
.gitignore
.test-passed
filter.mjs
/** Return the issues matching the selected status. */
export function filterIssues(issues, status = 'all') {
if (status === 'all') return issues;
return issues.filter(issue => issue.status === status);
}
filter.test.mjs
import test from 'node:test';
import assert from 'node:assert/strict';
import { filterIssues } from './filter.mjs';
const issues = [{ id: 1, status: 'progress' }, { id: 2, status: 'done' }];
test('all includes every issue', () => assert.equal(filterIssues(issues).length, 2));
test('in progress excludes completed issues', () => assert.deepEqual(filterIssues(issues, 'progress'), [issues[0]]));
test('done includes only completed issues', () => assert.deepEqual(filterIssues(issues, 'done'), [issues[1]]));
test('empty board stays empty', () => assert.deepEqual(filterIssues([], 'done'), []));
index.html
<!doctype html>
<html lang="en">
<meta charset="utf-8" /><meta
name="viewport"
content="width=device-width,initial-scale=1"
/><title>Issue board</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
background: #f2f3ef;
color: #1a1e1b;
font: 17px/1.6 system-ui;
}
header {
background: #fff;
border-bottom: 1px solid #d6d9d1;
padding: 20px 5%;
display: flex;
justify-content: space-between;
}
main {
max-width: 1050px;
margin: 60px auto;
padding: 0 28px;
}
small,
.muted {
color: #626b65;
}
h1 {
font-size: 48px;
letter-spacing: -2px;
margin: 8px 0;
}
nav {
display: flex;
gap: 8px;
margin: 32px 0 20px;
}
button {
font: inherit;
padding: 10px 22px;
border: 1px solid #ccd2c9;
border-radius: 9px;
background: white;
cursor: pointer;
}
button[aria-pressed="true"] {
background: #1f6b6b;
color: white;
border-color: #1f6b6b;
}
article {
display: flex;
align-items: center;
gap: 22px;
padding: 22px;
background: white;
border-bottom: 1px solid #e3e7df;
}
article:first-of-type {
border-radius: 12px 12px 0 0;
}
article:last-of-type {
border-radius: 0 0 12px 12px;
}
article[hidden] {
display: none;
}
article div {
flex: 1;
}
article strong {
display: block;
}
.pill {
font-size: 13px;
padding: 5px 12px;
border-radius: 18px;
background: #d9ecea;
color: #1f6b6b;
}
.done {
background: #e8eae4;
color: #4d584e;
}
.summary {
display: flex;
justify-content: space-between;
align-items: end;
}
footer {
margin-top: 25px;
color: #626b65;
font-size: 14px;
}
@media (max-width: 600px) {
main {
margin: 24px auto;
padding: 0 16px;
}
h1 {
font-size: 34px;
}
article {
padding: 16px;
gap: 12px;
}
button {
padding: 9px 12px;
}
small {
font-size: 12px;
}
}
</style>
<header>
<strong>◈ Issue board</strong
><span class="muted">Product / Autumn release</span>
</header>
<main>
<small>TEAM WORKSPACE / WEB APP</small>
<div class="summary">
<div>
<h1>A little more shipped.</h1>
<p class="muted">Track the details that make the product better.</p>
</div>
<span id="count">6 issues</span>
</div>
<nav aria-label="Issue status"><button data-status="all" aria-pressed="true">All issues</button><button data-status="progress" aria-pressed="false">In progress</button><button data-status="done" aria-pressed="false">Done</button></nav>
<article data-status="progress">
<small>WEB-142</small>
<div>
<strong>Add a status filter</strong
><small>Product experience · Alice</small>
</div>
<span class="pill">In progress</span>
</article>
<article data-status="done">
<small>WEB-139</small>
<div>
<strong>Remember the selected workspace</strong
><small>Navigation · Jamie</small>
</div>
<span class="pill done">Done</span>
</article>
<article data-status="progress">
<small>WEB-137</small>
<div>
<strong>Improve the empty state</strong
><small>Product experience · Alex</small>
</div>
<span class="pill">In progress</span>
</article>
<article data-status="done">
<small>WEB-134</small>
<div>
<strong>Make keyboard focus easier to see</strong
><small>Accessibility · Alice</small>
</div>
<span class="pill done">Done</span>
</article>
<article data-status="progress">
<small>WEB-131</small>
<div>
<strong>Polish the mobile layout</strong
><small>Responsive design · Jamie</small>
</div>
<span class="pill">In progress</span>
</article>
<article data-status="done">
<small>WEB-128</small>
<div>
<strong>Write the onboarding checklist</strong
><small>Documentation · Alex</small>
</div>
<span class="pill done">Done</span>
</article>
<footer>Demo project · Fictional people and issues</footer>
</main>
<script type="module">
import { filterIssues } from './filter.mjs';
const rows = [...document.querySelectorAll('article')];
const issues = rows.map((row, id) => ({ id, status: row.dataset.status }));
for (const button of document.querySelectorAll('nav button')) {
button.onclick = () => {
const visible = new Set(filterIssues(issues, button.dataset.status).map(issue => issue.id));
rows.forEach((row, id) => row.hidden = !visible.has(id));
document.querySelector('#count').textContent = visible.size + ' issues';
document.querySelectorAll('nav button').forEach(item => item.setAttribute('aria-pressed', String(item === button)));
};
}
</script>
</html>
package.json
{"name":"issue-board","private":true,"type":"module","scripts":{"test":"node --test filter.test.mjs && node -e \"require('fs').writeFileSync('.test-passed','yes')\""}}Recorded output of the approved command. This terminal does not take input.
$ npm test
> test
> node --test filter.test.mjs && node -e "require('fs').writeFileSync('.test-passed','yes')"
TAP version 13
# Subtest: all includes every issue
ok 1 - all includes every issue
---
duration_ms: 2.288419
type: 'test'
...
# Subtest: in progress excludes completed issues
ok 2 - in progress excludes completed issues
---
duration_ms: 1.867077
type: 'test'
...
# Subtest: done includes only completed issues
ok 3 - done includes only completed issues
---
duration_ms: 0.637119
type: 'test'
...
# Subtest: empty board stays empty
ok 4 - empty board stays empty
---
duration_ms: 0.399472
type: 'test'
...
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 497.635526
Apps running in the workspace.
node server.mjsOpens the issue board the agent changed. On this page it runs in a frame; in the app it opens at an address of its own that only you can open.
Add a status filterNeeds approval
MacBook is driving. Sending from here takes over.
The filter and tests are ready. Please approve npm test to check the four filter cases before reviewing the running app.
npm testWaiting for approval
Approval needed
Shell npm test
3 files changed+26−5
A replay drawn for this page, not the live app. The changes, files, test results and issue board come from a real run with demo data. The agent’s replies are scripted. Read it as text.
The conversation, files, terminal and changes stay together. Close your browser and return to the same workspace from another device.
See exactly what the agent changed in each file, beside the conversation, while it waits for your decision.

The command you approved, its output and the agent's summary stay in the session.

Ports lists the apps running in the workspace. Where previews are available, Open in browser shows one at an address of its own, which only you can open.

Use the running app before you accept the change. A preview is not a public link or a way to share with a team.

Each workspace lists its sessions and says whether it is running.

Get notified when your agent needs a decision, finishes a task or encounters a problem. Open the session to review the context and take the next step.
When an approval is needed, a task finishes or fails, an agent leaves a note or a workspace is ready. Turn notifications on for each device; on an iPhone, add Benchyard to the Home Screen first.
Simulated lock-screen notifications. Open the session to approve.
When an approval is needed, a plan is ready to review, a task fails or an agent needs you to sign in again. Finished tasks and agent notes are not emailed.
To: alice@example.com
An update for a decision that needs you.
Illustrated email using fictional data. You turn email on and confirm your address; the installation must have mail set up.
No separate app download.
Illustrative desktops. Notification badges depend on device support, permissions and settings.
Start with the agent you use today. Explore supported agents and upcoming integrations. Each supported agent keeps its own sign-in requirements and capabilities.
Connect a Claude subscription or an API key.
Sign in to ChatGPT with a one-time code where your account allows it, or use an API key.
Connect a Cursor account or an API key. Account sign-in may need renewing after about 60 days.
Connect with an API key. Subscription sign-in is not supported here.
Connect with a Kimi For Coding or Moonshot API key.
Integration is underway. Not yet available.
Planned integration. Not yet available.
Planned integration. Not yet available.
An agent needs your sign-ins and keys to do its work, but it does not need to see them. For protected credentials the agent gets a stand-in, and a credential keeper outside the workspace adds the real value only to requests for the service it belongs to.
INSIDE THE WORKSPACE
The agent can make a request without ever seeing the real credential.
[stand-in]CREDENTIAL KEEPER · OUTSIDE
It is added only to requests for the service the credential belongs to.
THE SERVICE
The service accepts the request and answers it.
This applies to protected credentials. A secret that is not tied to a service, a private key, and anything saved in a file in the workspace can still be read by agents.
Credentials are stored encrypted. Benchyard is not end-to-end encrypted: the party that runs the service can access what it stores.
Protected this way: agent sign-ins for Claude Code, Codex and Cursor, API keys for the services Benchyard knows, and any secret you tie to a service's address.
Your sign-ins, keys and secrets are stored encrypted, with a separate key for each person and team. Benchyard is not end-to-end encrypted: the party that runs the service can access what it stores.
A secret that is not tied to a service, a private key, and anything saved in a file in the workspace can be read by the agent.
Each workspace is isolated from the others. Where a workspace gives you administrator rights, an agent working for you has them too.
A workspace can reach the public internet, but not addresses inside private networks. Check your workspace's outbound traffic allowance and what happens when it runs out.
Sign in with a password or a passkey, with two-factor authentication. Review your signed-in devices and secure your account from Settings.
Share the setup your team needs while keeping workspaces individually owned.
Share rules, skills and tools. A shared tool that runs code reaches a member only after that member allows it.
Provide the agent sign-ins your team's workspaces use, and see usage across the team.
A team admin cannot open a member’s workspace contents or start and stop their workspaces.

Contact us about a hosted account or an installation on infrastructure you operate. We agree the shape before anything starts.
Agents, workspace sizes, limits and how the installation is set up are agreed for your business. This page is not a catalog, and it does not promise a feature that is not agreed.
Benchyard can run on infrastructure your business operates. That installation is licensed separately from the hosted service, and the operator's own privacy notice applies to it.
Availability depends on what we agree. Agent subscriptions and API usage stay with their providers.
With device push enabled, supported events include approval requests, plans ready for review, task completion or failure, agent notes and workspace readiness. Turn notifications on for each device. Delivery depends on your settings; email covers fewer events. Open the session to review and act.
Teams share instructions, skills, tools and managed agent sign-ins, and see usage. Workspaces remain individually owned. A shared tool that runs code needs each member’s permission.
Team administration does not grant access to another member’s workspace or private previews. The service is not end-to-end encrypted: its operator can access stored data. Credential protection applies to supported credentials, not every secret or file.
Benchyard provides secure cloud workspaces for supported agents and teams. It keeps the conversation, the files, a terminal, the changes and a preview of the running app together, and you reach it from a computer or a phone.
Yes, while the workspace is running. Closing the browser does not stop it. A task that needs your approval waits for your decision; stopping the workspace ends what was running in it.
Its files, conversations and sign-ins are kept; terminals and running programs end. Opening a session does not start it. Sending a message does, and the message is held until the workspace is up. An idle machine may be suspended instead, and then what was running carries on when it starts again.
Yes. One device drives a session at a time, and any other device you are signed in on can watch and approve. Sending a message from another device takes the session over. Open the session from a notification to make the decision.
No. An agent is the tool that carries out tasks; a model is the AI behind it. Which models you can use depends on the agent and how it is set up. The agents shown here are the current ones, and more are planned.
Yes. Bring the subscription or API key your agent accepts, or use a sign-in your team provides. What the agent's provider charges is separate from Benchyard.
Where your machine type includes it, yes: you and your agents get sudo without a password. On a machine, what you install as root stays; in a container only /workspace is kept, so it lasts until the workspace restarts. Without sudo, mise, pixi and uv install into your home. Look for sudo in the workspace type’s features.
Yes. Install the benchyard command, sign in with it and add your public key, then ssh to the workspace by its name or open it in VS Code or Cursor over Remote SSH. benchyard forward makes a port of the workspace a localhost port on your computer.
No. A preview opens only for the workspace's owner, signed in. It is not a public link or a way to share with a team. The Ports panel shows when a browser preview is available.
If you turn email on, you get one when an agent needs your approval, has a plan for you to review, fails, or needs you to sign in again. Finished tasks and agent notes are not emailed.
Workspace usage is counted in credits. Check the type’s rates before starting. Agent subscriptions and API usage are separate and stay with their providers.
Registration is open. Choose Sign up, enter your email and follow the link in your inbox to create your account. Eligible new accounts receive 3 trial credits, valid for 7 days after they are granted. If a friend shared a referral link, open it before signing up to qualify for 2 extra credits, valid for 30 days.
Yes, as an installation you agree, on infrastructure you operate. You can also use the hosted service. Contact us with the business, where it should run, and what should be tailored.
Looking for details? Read the workspace guide, agent guide and security guide.
Start personally, share a setup with your team, or talk to us about a tailored deployment.
Pay as you go
Or choose a personal plan
A persistent place for your projects and agents.
Per-seat plans
Shared billing for your team
Share tools and billing while keeping each member's workspace private.
Custom pricing
Tailored to your requirements
Agree the deployment, capacity and setup your business needs.
Current credit prices, currencies, plans and machine rates are shown in the console. Review them before buying or starting a workspace.
Your agent subscription or API usage is billed separately by its provider.
For eligible new accounts after email confirmation. Use them within 7 days of receiving them. The trial is available once per account and email address.
Have a friend on Benchyard? Open their referral link before signing up. Eligible new accounts receive 2 more credits alongside the trial; these extra credits last 30 days.
Confirm your email within 7 days of creating your account to receive the trial. Your Billing page shows the rewards you qualify for and when they expire.
Check Billing for the rewards available to your account, their amounts and expiry dates. When offered, you can:
Create your first workspace and start your first session in it to earn first-step rewards. Complete eligible steps after confirming your email and within 30 days of creating your account.
Share your link from Billing, Invite friends. A first sign-up can earn a first-step reward; a friend's qualifying first paid purchase can earn you referral credits. Monthly limits apply.
An eligible first purchase of credits or a plan can come with bonus credits. Check the offer before paying; the bonus is based on the part you pay for after any discount.
Redeem a credit code in Billing, or apply a discount code when reviewing an order. Each code has its own eligibility, expiry and usage limits.
Promotional credits are used before purchased credits, expire on their stated dates and cannot pay for plans or seats. They have no cash value. Refunded purchases can reverse related bonuses and referral rewards.