A Look at Hook Execution in Orca's orca.yaml

·4 min read

AI developer tools are increasingly executing logic directly from repositories—setup steps, configuration, and hooks.

While looking through Orca, I came across a simple example of repository-defined configuration becoming local shell execution.


Context

Tools like Orca are doing more than traditional editors. In many cases, they:

  • Clone repositories
  • Parse configuration files
  • Run setup steps
  • Execute project-defined hooks

This makes workflows smoother, but it also means repository content can influence what runs locally.


What I Observed

While exploring the codebase, I was generally looking for:

  • External API calls
  • Interactions with local services
  • Calls to exec or other process-spawning functions

Following that path led to how Orca handles hooks defined in orca.yaml.

These hooks are executed using:

  • exec(script, { shell: ... })
  • execFile(... 'bash', '-c', ...) (via WSL)

In other words, commands defined in the repository are executed directly in a local shell environment.

A minimal example:

# Example orca.yaml snippet
scripts:
  setup: |
    echo "setup hook executed"
    open -a Calculator

When a repository defines a hook like this, that command runs locally as part of the workflow.

In practice, this ran automatically during normal worktree lifecycle actions like:

  • Creating a worktree
  • Deleting a worktree

In my test case, this meant the Calculator app would open as those actions occurred.

This creates a simple but important behavior:

Opening or working with a repository can trigger execution defined entirely within that repository.


Implementation Detail

At a high level, the flow looks like:

repository config → hook resolution → shell execution

In code, that starts with resolving the hook from repository configuration:

const hooks = getEffectiveHooks(repo)
const script = hooks?.scripts[hookName]
 
if (!script) {
  return Promise.resolve({ success: true, output: '' })
}

Standard (macOS / Linux) Execution Path

In the standard execution path, the script is passed directly into a shell:

exec(script, { cwd, timeout: HOOK_TIMEOUT }, (error, stdout, stderr) => {
  // handle result
})

This is the key boundary: the contents of orca.yaml become shell input.

At its simplest, this becomes:

repository-controlled string → shell execution

Windows / WSL Execution Path

In WSL environments, the same script is routed through bash -c:

const escapedCwd = wslInfo.linuxPath.replace(/'/g, "'\\''")
const escapedScript = script.replace(/'/g, "'\\''")
const bashCmd = `cd '${escapedCwd}' && ${escapedScript}`
 
execFile(
  'wsl.exe',
  ['-d', wslInfo.distro, '--', 'bash', '-c', bashCmd],
  {
    timeout: HOOK_TIMEOUT,
    encoding: 'utf-8',
    env: { ...process.env, ...wslEnv }
  },
  ...
)

Why It's Interesting

This isn’t unique to Orca—many tools support hooks or setup scripts.

What stood out is how naturally execution becomes tied to normal workflows like creating or operating on a worktree, rather than an explicit action like running a script.


Contribution

I opened a PR to help refine how this behavior is handled:

https://github.com/stablyai/orca/pull/1138

Huge thanks to the Orca / Stably AI team for being responsive and thoughtful. The maintainer jumped on this quickly and did the bulk of the work to refine the overall approach.

The resulting changes introduced a more explicit trust model around hook execution:

  • Hook execution is gated behind a prompt the first time it runs in a repo (per hook type)
  • Approvals are persisted using a content hash of the script, so unchanged hooks don’t re-prompt across restarts
  • If orca.yaml changes, the hash no longer matches and the user is prompted again with the updated script
  • An “always trust this repository” option allows users to bypass all future hook prompts for that repo

This keeps hooks as a useful feature, while making their execution more visible, intentional, and consistent.


Broader Observation

As more tools move toward automation and agent-like workflows, they increasingly:

  • Treat configuration as executable behavior
  • Run steps on behalf of the user
  • Optimize for reducing manual setup

That's a powerful shift, but it makes trust boundaries less obvious by default.


Takeaway

For tools in this space, it's worth being deliberate about:

  • When repository-defined logic is executed
  • How visible that execution is to the user
  • Where explicit trust decisions happen

Small implementation details like this can have a meaningful impact on how these tools behave in practice.