Skip to content

feat(hooks): add Factory Droid integration#2267

Merged
aeppling merged 7 commits into
rtk-ai:developfrom
krimvp:feat/droid-integration
Jul 8, 2026
Merged

feat(hooks): add Factory Droid integration#2267
aeppling merged 7 commits into
rtk-ai:developfrom
krimvp:feat/droid-integration

Conversation

@krimvp

@krimvp krimvp commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Closes #2099

Summary

Adds first-class Factory Droid support so shell commands the agent runs are transparently rewritten to their token-saving rtk equivalents.

  • rtk init --agent droid — installs a native PreToolUse hook (matcher Execute) into Droid's canonical hooks.json (~/.factory/hooks.json, or project .factory/hooks.json), falling back to the hooks key of settings.json only when that file already carries live PreToolUse hooks. Supports global (-g) and project scope, the FACTORY_HOME_OVERRIDE env override, idempotent install with .bak backup + atomic write, and a clean uninstall round-trip (--uninstall).
  • rtk hook droid — processes Droid's PreToolUse payload (BOM strip, stdin cap, JSON-parse fallback), mirroring the existing provider hooks.
  • Droid's own permissions are enforced (review feedback): verdicts come from permissions::Host::Droid, sourced from ~/.factory/settings.json — never from another agent's settings.
    • commandAllowlist → rewrite carries permissionDecision: "allow" (the verdict Droid would emit natively). Droid's built-in default allowlist (ls, pwd, git status, git log, …) is mirrored, and a user list replaces its default outright — matching Droid's own settings.commandAllowlist || DEFAULT accessor.
    • commandDenylist (always confirm) and commandBlocklist (never runs) → RTK steps aside entirely so Droid's native confirm/block fires on the original command. Rewriting first would dodge Droid's own pattern matching (rtk git log no longer matches a git log denylist entry).
    • Anything unlisted → rewrite with no decision, landing via Droid's "updated input result" path while Droid's native prompt and other hooks' deny/ask stay in control.

Protocol verified against Factory's hooks reference and the shipped Droid CLI (v0.153.1): Execute matcher, tool_input.command, hookSpecificOutput with permissionDecision/updatedInput, per-event-key merge of hooks.json over settings.json hooks, FACTORY_HOME_OVERRIDE as a home-dir override, and the commandAllowlist/commandDenylist/commandBlocklist defaults in settings.

Demo

End-to-end: install → Droid rewrites & auto-runs allowlisted commands at --auto low → a denylisted command is left to Droid's native permission gate → savings in rtk gain → uninstall.

RTK x Factory Droid demo

If the GIF doesn't load inline: https://files.catbox.moe/cxbk1y.gif (mirror: https://gist.github.com/krimvp/cfc67a932bf7e788cdfebcea4b6f596b)

Test plan

  • cargo fmt --all clean
  • cargo clippy --all-targets — zero warnings
  • cargo test --all — 2394 passed (34 Droid-specific: install/uninstall round-trip, idempotency, matcher-group reuse, hooks.json/settings.json target resolution, allowlist auto-allow, denylist/blocklist step-aside, user-list-replaces-default, deny step-aside, legacy Bash matcher, passthrough)
  • Verified live against the real droid CLI (v0.153.1), all with --auto low:
    • git status / git log rewritten to rtk … and auto-run via Droid's default allowlist (session transcript shows RTK's filtered output; savings tracked in rtk gain)
    • with "commandDenylist": ["git log"]: RTK emits nothing, Droid natively refuses (insufficient permission to proceed) — A/B control with the entry removed runs rewritten again
    • Claude Code settings no longer leak: a Bash(curl:*) allow rule in ~/.claude/settings.local.json no longer auto-allows curl in Droid
  • settings.json untouched by install/uninstall round-trip

Docs

README updated — Supported Agents list + integration table row. docs/guide/getting-started/supported-agents.md updated per review feedback (frontmatter, agents table, installation section).

🤖 Generated with Claude Code

@CLAassistant

CLAassistant commented Jun 4, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@krimvp krimvp marked this pull request as ready for review June 4, 2026 15:09
@aeppling aeppling self-assigned this Jul 1, 2026
@aeppling

aeppling commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Hey @krimvp

Thanks for this contribution, this PR supersed #1308 and #912 , looking forward this one now because more complete and up to date

krimvp added 4 commits July 4, 2026 21:16
Register RTK with Factory Droid so shell commands the agent runs are
transparently rewritten to their token-saving `rtk` equivalents.

- `rtk init --agent droid`: install a native PreToolUse hook into
  ~/.factory/settings.json (matcher "Execute"); supports global (-g) and
  project scope, the FACTORY_HOME override, idempotent install with backup +
  atomic write, and a clean uninstall round-trip.
- `rtk hook droid`: process Droid's PreToolUse payload (BOM strip, stdin cap,
  JSON-parse fallback), mirroring the existing provider hooks.
- Deny verdict steps aside (no output) so Droid's native deny handling fires,
  matching Claude/Cursor/Copilot and the PermissionVerdict::Deny contract.
- Rewrites are auto-allowed: Droid only applies a hook's updatedInput when the
  decision is "allow", so we mirror run_cursor to avoid silently dropping the
  rewrite (and its savings) on the default verdict.
Droid 0.140.0 applies a PreToolUse hook's updatedInput regardless of the
permission decision (verified empirically + via binary decompile), so the
forced permissionDecision:"allow" was unnecessary. Worse, Droid resolves
the decision as the first hook result carrying one (no deny-over-allow
priority), so an unconditional allow could suppress another PreToolUse
hook's deny/ask and bypass Droid's native prompt.

Now omit permissionDecision by default and only assert allow on an explicit
allow rule, mirroring process_claude_payload. The rewrite still lands via
Droid's decision-independent "updated input result" path. Updated the
inaccurate comment and the test that required allow.
Adopt the decide_from_verdict/HookDecision flow introduced on develop so
the Droid handler gains the same safety behavior as Claude/Cursor/Copilot:
commands with substitution or file redirects now Defer (no rewrite) instead
of being rewritten, and deny/allow/ask mapping is shared instead of
reimplemented. Also removes an unwrap() in the response builder.

Claude-Session: https://claude.ai/code/session_01Vp4p5YroP2cfFvThhQE4Rv
Adversarial verification against Droid v0.164.0 (shipped code + docs)
found two broken assumptions in the original integration:

- Droid's canonical hooks file is hooks.json (root event map, no wrapper);
  the hooks key of settings.json is only a fallback that hooks.json shadows
  per event key. Droid's own /hooks UI writes hooks.json, so an RTK entry
  in settings.json silently dies as soon as the user adds any PreToolUse
  hook there. Install now targets the file whose PreToolUse array Droid
  actually reads (live hooks.json > live settings.json fallback > create
  canonical hooks.json), migrates stale copies, and uninstall sweeps root
  hooks.json, legacy hooks/hooks.json, and settings.json.

- The config-home env var is FACTORY_HOME_OVERRIDE (replaces $HOME, with
  .factory appended), not FACTORY_HOME pointing at the config dir.

Also corrects the 'legacy Bash matcher' comment (Droid never had a Bash
tool; the acceptance is purely defensive) and records that the
omit-permissionDecision rewrite path is confirmed in v0.164.0: decisions
resolve as find(first result with a decision) and updatedInput applies
via a separate path even when no decision is set.

Claude-Session: https://claude.ai/code/session_01Vp4p5YroP2cfFvThhQE4Rv
@krimvp krimvp force-pushed the feat/droid-integration branch from 5106c44 to 77d9ee4 Compare July 4, 2026 21:33
Comment thread src/hooks/init.rs Outdated

/// Install Factory Droid PreToolUse hook into `settings.json`.
///
/// - Global (`-g`): writes to `~/.factory/settings.json` (or `$FACTORY_HOME/settings.json`).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the Droid CLI documentation:

Droid hooks live in hooks.json
- ~/.factory/hooks.json – User (applies to all projects)
- .factory/hooks.json – Project (commit to share)
- enterprise managed policy hooks

If hooks.json is absent, Droid falls back to hooks declared under the hooks key in the matching settings.json.

So it seems that settings.json is a fallback.
We should try to use this file and fallback to settings if not available to better match Droid CLI behavior ?

source : https://docs.factory.ai/reference/hooks-reference

Comment thread src/hooks/hook_cmd.rs Outdated
ti
};

// Wire format mirrors Claude's `hookSpecificOutput`; the allow policy mirrors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too verbose comment, if need to refer to a agent documentation or behavior to specific version, you can link a documentation or changelog

Just to keep it clean :)

Comment thread src/hooks/hook_cmd.rs Outdated
"updatedInput": updated_input
});

if verdict == PermissionVerdict::Allow {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leak claude settings because using the check_command that check for claude code settings.

Droid has its own permissions through commandAllowlist / commandDenylist / commandBlocklist and autonomyLevel in ~/.factory/settings.json

RTK should respect the agents permissions system and always match the verdict (allow/deny or ask) that the agent would have natively emitted

source : https://docs.factory.ai/cli/configuration/settings

Comment thread README.md
rtk init --agent antigravity # Google Antigravity
rtk init -g --agent pi # Pi
rtk init --agent hermes # Hermes
rtk init -g --agent droid # Factory Droid

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

krimvp added 2 commits July 5, 2026 23:12
The Droid hook consulted Claude Code's settings (Host::Claude), leaking
one agent's permission config into another: a Bash(...) allow rule in
~/.claude/settings.json could auto-allow commands in Droid, bypassing
its native prompt.

Add permissions::Host::Droid backed by ~/.factory/settings.json
(honoring $FACTORY_HOME_OVERRIDE as a home-dir override, matching the
shipped Droid CLI):

- commandAllowlist -> Allow: the rewrite carries permissionDecision
  "allow", exactly the verdict Droid would emit natively.
- commandDenylist (always confirm) and commandBlocklist (never runs)
  -> Deny: RTK steps aside entirely so Droid's native confirm/block
  fires on the original command. Rewriting first would dodge Droid's
  own pattern matching ("rtk git log" no longer matches a "git log"
  denylist entry).
- Built-in defaults are mirrored from the shipped Droid CLI (verified
  against v0.153.1); a user list replaces its default outright, same
  as Droid's own accessor.

Also trims the wire-format comment per review feedback.

Verified live against droid 0.153.1: git status/git log rewrite and
auto-run at --auto low via the default allowlist; a denylisted git log
is left untouched and natively refused; uninstall round-trip clean.

Claude-Session: https://claude.ai/code/session_015CvKLxKMnmcvwY5CjHVTF5
Propagates the README Droid documentation to
docs/guide/getting-started/supported-agents.md per review feedback:
frontmatter description, agents table row, and an installation section
covering hooks.json placement and Droid-native permission handling.

Claude-Session: https://claude.ai/code/session_015CvKLxKMnmcvwY5CjHVTF5
@krimvp krimvp force-pushed the feat/droid-integration branch from 8623937 to 0a032aa Compare July 5, 2026 21:38
Comment thread src/hooks/permissions.rs Outdated
"rm -rf /",
"rm -rf /*",
"rm -rf .",
"rm -rf ~",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those kind of list are often a bad idea for maintenance and transparence.

Drop DROID_DEFAULT_ALLOWLIST / DROID_DEFAULT_DENYLIST and fall back to empty lists. Hardcoding defaults will drift against Factory's releases and makes RTK auto-allow commands the user never listed.

To keep : (1) read all scopes global + settings.local.json + project .factory/ (Droid merges them; see load_gemini_rules' folderTrust handling), not global-only, or project/local deny entries get dodged; (2) honor only explicit commandDenylist/commandBlocklist → step-aside (the fix for the rtk-rename dodge).

For the allow side, prefer defer (no permissionDecision) rather than emitting allow, since RTK renaming the program to rtk means it can't transparently reproduce Droid's own allow decision.

If any question do no hesitate to tag me so we can cleanly implement this integration

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 3d40742 — thanks for the detailed guidance @aeppling.

  • Dropped DROID_DEFAULT_ALLOWLIST/DROID_DEFAULT_DENYLIST entirely; no built-in lists are mirrored anymore.
  • Deny-only: RTK reads explicit commandDenylist/commandBlocklist from all four scopes (~/.factory/settings.json + settings.local.json, project .factory/settings.json + settings.local.json) and steps aside on a match. One deliberate divergence: entries are unioned across scopes rather than replicating Droid's replace-on-merge precedence — over-collecting only causes a spurious step-aside (Droid still decides natively on the original command), while under-collecting would reopen the rename dodge.
  • Allow side: RTK never emits permissionDecision — rewrites land via updatedInput alone and the verdict stays with Droid's native flow (allowlist, autonomy level, other hooks).

Verified end-to-end against Droid CLI 0.153.1: Droid evaluates its permission lists on the original command before applying updatedInput, so native allowlist decisions are unaffected (git status still auto-runs at --auto low) and denylisted commands are gated natively with no rewrite involved.

demo

…scopes

Drop the hardcoded mirrors of Droid's built-in command lists and stop
emitting permissionDecision entirely. RTK now steps aside only on
explicit commandDenylist/commandBlocklist entries, unioned across all
four settings scopes (user + project, settings.json +
settings.local.json); every other command is rewritten via updatedInput
and the decision is left to Droid's native flow.

Verified against Droid CLI 0.153.1: updatedInput is applied after
Droid's own permission evaluation of the original command, so native
allowlist decisions are unaffected and deny entries fire natively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpSp3Arj7fG1XJYhXoCMGC
@aeppling

aeppling commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Hey @krimvp , LGTM , thanks for contributing to RTK with this new integration !

@aeppling aeppling merged commit 8a8b356 into rtk-ai:develop Jul 8, 2026
11 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support for Factory Droid CLI

3 participants