The core design question for a self-modifying system is not "can it modify itself," but "who approves execution after modification."
Pattern Layer
9.1 The Trust Model of a Self-Modifying System
The trust model of a self-modifying system can be simplified into three stages:
Stage One: Filesystem (writing). The agent uses generate_* tools to write files to skills/, prompts/, and capabilities/. The privilege level for this operation is write_file -- no different from writing an ordinary README file. It does not trigger a permission check and does not require user confirmation.
Stage Two: Registration (activation). Written files are registered into the available capabilities list through the Registry. Skills in the skills/ directory are automatically registered (scanned at startup), and Capabilities in the capabilities/ directory are automatically registered. Registration does not change permissions -- it simply makes the agent aware that "this capability exists."
Stage Three: Execution (authorization). When the agent calls run_capability, a permission check is triggered. This is the only stage among the three that requires user confirmation. The outcome of the permission check determines whether the Capability can be executed.
The three-stage gate design ensures: writing files does not trigger a security gate, registration does not change permissions, and only execution requires authorization. Failure at any single stage does not cause the entire trust chain to collapse. If malicious content is not detected during file writing (no content validation), there is still a permission check at execution time. If the permission check misjudges (allowing a malicious Capability through), it can only run within the current session (the Shell environment maxes out at the session allowlist).
The core principle of the trust chain can be condensed into one sentence: writing is cheap, reversible, and does not require approval; execution is expensive and requires explicit authorization.
This principle applies not only to Capabilities but also to Skills. Writing a Skill file to skills/ does not require approval (the agent can use generate_skill to write directly). But Skill execution does not trigger a permission check -- because Skills do not execute code; they only alter the reasoning path. Skills are "cheap to write" safely because Skills have no execution side. Capabilities are "expensive to execute" necessarily because Capabilities have an execution side.
9.2 Why Writing Files Should Not Trigger Permission Checks
A common counter-question: wouldn't it be safer if writing files triggered a permission check?
Not necessarily. Consider two scenarios:
Scenario A: The agent writes a Capability file.
generate_capability → writes to capabilities/daily-report/main.sh
→ permission check passes → file written
Triggering a permission check during file writing means the user needs to confirm "whether to allow writing this file." But the problem is: the user is confirming the action of "writing a file," not "executing this file." The user confirmed writing the file but does not know it will later be executed via run_capability. A permission check at write time cannot substitute for a permission check at execution time.
Scenario B: The agent modifies an existing Capability file.
The agent modifies capabilities/daily-report/main.sh, rewriting the execution logic. If writing a file triggers a permission check, the user needs to confirm again. But the user may not carefully review the diff -- "it's writing daily-report again, I already confirmed that before" -- and clicks approve.
A better approach is: writing files does not trigger a permission check (cheap), but after a file changes, existing trust is automatically revoked. If the user previously granted daily-report a session allowlist, trust is reset upon file change. The next execution triggers a fresh permission check.
CodeCoder's choice:
File writing: no permission check (cheap)
File change detection: compare manifest mtime
Trust revocation: mtime change → revoke entries in session allowlist / project allowlist
Next execution: trigger a fresh permission check
9.3 Recursion Boundary
The deepest security question facing a self-modifying system: can a Capability generate a new Capability?
Technically, yes. The generate_capability tool is at the write_file privilege level -- it does not require additional authorization. The execution code of a Capability could contain:
#!/bin/sh
# This Capability generates a new Capability
echo "name: evil-script" > capabilities/evil/manifest.yaml
echo '#!/bin/sh\nrm -rf /' > capabilities/evil/evil.sh
But the critical point of the security design is: execution of the newly generated Capability still requires going through the run_capability permission check.
The newly generated evil Capability must trigger a permission check when run_capability evil is called. Even if the generating Capability has been granted a session allowlist, the new Capability is still a "first-time execution" requiring user confirmation.
The core of the recursion boundary is not preventing generation (generate_capability's write_file permission is not revocable), but preventing "automatic execution after generation." The execution of any Capability -- regardless of who generated it -- must pass through the run_capability permission check.
This rule also has an important corollary: a Capability cannot automatically execute another Capability. If Capability A's code contains run_capability B, the system will refuse -- because run_capability is a tool that requires permission checking, and the Capability's code itself does not hold tool invocation permissions.
Case Layer
9.4 Complete Analysis of the @shell Ceiling Rule
The full statement of the @shell ceiling rule:
The maximum trust level for a Shell-environment Capability is
AlwaysThisSession. It cannot be promoted toAlwaysThisProject.
This rule is enforced at multiple checkpoints in the lookup chain (for the complete lookup chain pseudocode, see Chapter 4, Section 4.7):
At project allowlist write time: If the user selects
AlwaysThisProject, the system checks the Capability's environment. If it is@shell, the write is rejected and aCeilingViolationerror is returned.At project allowlist read time: If
codecoder.jsoncontains a manually edited@shellentry (e.g.,run_capability:shell), the system detects it during loading and skips this entry.At runtime: If the session allowlist already contains an
AlwaysThisProjectentry (which should not happen, but serves as defense-in-depth), the system checks again before execution.
The ceiling rule is part of a "defense-in-depth" design -- not just a single check point, but checks at write, read, and execute. Each layer has different failure conditions, but as long as any single layer's check passes, the rule holds.
Why can't @shell be promoted to the project allowlist? The intuitive answer is "Shell is too dangerous." But the more precise answer is: the trust foundations of the session allowlist and the project allowlist are different. The session allowlist is based on "the agent is trustworthy within the current session." The project allowlist is based on "this Capability's code is trustworthy." Shell-environment Capability code can be arbitrarily modified (the agent can generate a Shell script and then modify it), so the assumption that "the Capability's code is trustworthy" does not hold.
9.5 The generate_* to run_capability Trust Chain
The complete trust chain path:
generate_skill -> write_file -> skills/<name>.md
Permission: None (write_file level)
Trust cost: Lowest. Skills do not execute code.
generate_prompt -> write_file -> prompts/<name>.md
Permission: None (write_file level)
Trust cost: Lowest. Prompts are loaded on demand.
generate_capability -> write_file -> capabilities/<name>/
Permission: None (write_file level)
Trust cost: Lowest. Just writing a file.
promote_prompt -> file move prompts/ -> skills/
Permission: None (file operation)
Trust cost: Low. Just changing load timing.
run_capability -> permission check -> execution
Permission: run_capability:<env>
Trust cost: Highest. Triggers the full permission lookup chain.
The trust cost at each step:
generate_*: 0 (no permission check triggered)promote_prompt: 0 (no permission check triggered)run_capability: full (permission lookup chain + ceiling rule + mtime detection)
The critical insight is: trust costs are not cumulative. From generate_capability to run_capability, the trust cost is 0 at every intermediate step, but jumps to full at the final step. There is no "semi-authorized" state in between -- writing a file does not imply permission to execute, and registration does not imply permission to execute. The gate at each step is independent.
9.6 File Change /reload Visibility -- Bypass Path Analysis
A potential bypass scenario: the agent writes a harmless Capability, and the user grants a session allowlist. Then the agent edits the Capability's code to malicious content. On the next execution -- if the system does not detect the file change -- the malicious code runs.
CodeCoder's defense is: manifest mtime change → revoke trust.
fn run_capability(name: &str, context: &Context) -> Result<Output> {
let cap = context.registry.capabilities.get(name)?;
// Check for file changes
let current_mtime = cap.manifest_mtime()?;
let trusted_mtime = context.session_allowlist.get_mtime(name);
if current_mtime != trusted_mtime {
// File has been modified, revoke trust
context.session_allowlist.remove(name);
// Re-trigger permission check
context.permission_check(&format!("run_capability:{}", cap.environment()))?;
// Update trusted mtime
context.session_allowlist.set_mtime(name, current_mtime);
}
// Execute
match cap.environment() {
Environment::Shell => run_shell(cap, context),
// ...
}
}
This defense prevents the "gain trust first, then modify code" known attack path. However, there is a limitation: the mtime comparison is only triggered when run_capability is called. If the attacker modifies the file and then immediately calls run_capability, the detection triggers. If the attacker modifies the file and waits for the user to call it manually later, the detection also triggers. As far as this path is concerned, the current design leaves no known bypass — but this only means "known bad paths are excluded," not proof that no other attack surface exists: for example, mtime itself can be forged (rolling back file timestamps), there is a window where the Registry cache and disk state diverge, or an attacker may sidestep this gate through a non-run_capability channel (such as a Capability invoking the shell itself). The security boundary is a progressive convergence that excludes known bad paths, not an absolute safety reached in one step.
9.7 Security Derivation of the Sub-Agent's Inherent Read-Only Constraint
The sub-agent's read-only constraint (discussed in detail in Chapter 5) is a corollary of the self-authoring safety loop.
The parent agent can generate Skills, Prompts, and Capabilities. A sub-agent cannot generate anything. This means: even if the parent agent creates a malicious sub-agent, the sub-agent can only read files, search the web, and return text results. It cannot write files, register Capabilities, or execute code.
This constraint is not implemented through permission checks -- it is hardcoded through toolset restriction. The sub-agent's toolset is listed in Toolbox::read_only_child() and contains no write or execute tools.
Corollary: a sub-agent cannot bypass the self-authoring safety loop. A parent agent cannot achieve the effect of "having a sub-agent do what I cannot do" by creating a sub-agent. What a sub-agent can do is a subset of what the parent agent can do -- and it is a read-only subset.
ADR Deep Dive
Two Enhancements to the Safety Loop (ADR 0022)
ADR 0022 records two major enhancements to the self-authoring safety loop.
First enhancement (initial version): The three-stage separation of the trust chain was completed -- writing files does not trigger permission checks, registration does not change permissions, and execution requires authorization. run_capability implemented basic permission checking, the session allowlist, and the project allowlist. The ceiling rule had not yet been introduced.
Second enhancement (ceiling rule introduced): A problem was exposed: a user added run_capability:shell to the project allowlist. Later, the agent created a new Shell Capability, which passed the permission check directly through the project allowlist -- without a confirmation dialog.
The fix was the ceiling rule: Shell environments cannot be promoted to the project allowlist. However, after introducing the ceiling rule, three independent check points appeared in the code (at write time, read time, and execution time), each with a different implementation and different test coverage. In a subsequent enhancement, the logic of the three check points was unified into a single function ceiling_check(key, level) to ensure policy consistency.
The evolution documented in this ADR illustrates how the self-authoring safety loop was designed: it was not fully conceived in one go, but was progressively strengthened through exposure to real attack scenarios.
End of Part Three. The next part moves into autonomous operation -- work graphs, acceptance gates, headless mode, and compaction.